Shell脚本如何实现读写锁同步

wen 实用脚本 5

Shell脚本如何实现读写锁同步:高并发场景下的脚本安全指南

📚 目录导读

  1. 为什么Shell脚本需要读写锁?——并发读写引发的数据混乱场景
  2. 读写锁核心原理——读写互斥、读读并行的逻辑解析
  3. 纯Shell实现读写锁的三种方法(含代码实例)
  4. 实战:文件级读写锁与进程间同步
  5. 常见问题与避坑指南(问答形式)
  6. 性能优化与安全建议

为什么Shell脚本需要读写锁?

在许多自动化运维脚本中,多个进程同时访问同一资源(如配置文件、数据库备份文件、日志)时,若不加控制会导致:

Shell脚本如何实现读写锁同步

  • 写写冲突:两个进程同时写文件,内容交错损坏
  • 读写冲突:一个读进程读到一半,另一个写进程修改了数据,读到脏数据

经典场景:服务器监控脚本每分钟读取配置文件,而另一脚本每小时更新配置——没有锁机制,读到的配置可能半旧半新。

读写锁(Reader-Writer Lock) 正是解决此类问题的关键:允许多个读进程并发,但写进程必须独占。


读写锁核心逻辑(5秒理解)

操作类型 能否与读共存 能否与写共存
读操作(Read) ✅ 允许 ❌ 禁止
写操作(Write) ❌ 禁止 ❌ 禁止

在Shell中,通常依靠文件锁(flock)临时文件标记来模拟这一行为。


纯Shell实现读写锁的三种方法

基于flock命令(推荐,最可靠)

flock直接使用系统级文件锁,能有效处理进程退出时自动释放锁。

# 定义一个锁文件
LOCK_FILE="/tmp/myapp.lock"
# 获取读锁(共享锁)
read_lock() {
    exec 200>$LOCK_FILE
    flock -s 200  # -s 表示共享锁,读锁
    echo "读锁已获取,PID:$$"
}
# 获取写锁(独占锁)
write_lock() {
    exec 201>$LOCK_FILE
    flock -x 201  # -x 表示独占锁
    echo "写锁已获取,PID:$$"
}
# 释放锁
unlock() {
    flock -u 200 2>/dev/null
    flock -u 201 2>/dev/null
}
# 使用示例
read_lock
# ... 读取配置文件 ...
unlock
write_lock
# ... 写入新配置 ...
unlock

优点:系统级支持,避免死锁,无需手动创建信号文件。

基于目录原子操作(无flock时备用)

利用mkdir的原子性(同一目录只能被一个进程创建成功)。

LOCK_DIR="/tmp/rwlock"
read_lock() {
    while ! mkdir "$LOCK_DIR.read" 2>/dev/null; do
        # 检查是否正在写(写锁会创建 .write 目录)
        if [ -d "$LOCK_DIR.write" ]; then
            sleep 0.1
            continue
        fi
        # 尝试获取读锁(读锁数++
        mkdir "$LOCK_DIR.read.$$" 2>/dev/null && break
    done
}
write_lock() {
    # 必须等待所有读锁释放,且无其他写锁
    while ! mkdir "$LOCK_DIR.write" 2>/dev/null; do
        sleep 0.1
    done
    # 检查是否还有读锁进程
    while ls "$LOCK_DIR.read"* 2>/dev/null | grep -q .; do
        sleep 0.1
    done
}
# 释放
read_unlock() {
    rmdir "$LOCK_DIR.read.$$" 2>/dev/null
    # 没有更多读锁时,清理读标记
    rmdir "$LOCK_DIR.read" 2>/dev/null
}
write_unlock() {
    rmdir "$LOCK_DIR.write" 2>/dev/null
}

简单计数文件(教学用,不推荐生产)

创建/tmp/reader_count文件记录读进程数。

READ_COUNT_FILE="/tmp/reader_count"
write_lock() {
    while [ -f "$READ_COUNT_FILE" ] || \
          ! ln "$READ_COUNT_FILE" "$READ_COUNT_FILE.lock" 2>/dev/null; do
        sleep 0.1
    done
    echo "写锁"
}
read_lock() {
    # 检查写锁是否存在
    while [ -f "$LOCK_DIR.write" ]; do
        sleep 0.1
    done
    # 递增读计数
    echo $(( $(cat $READ_COUNT_FILE 2>/dev/null || echo 0) + 1 )) > $READ_COUNT_FILE
}

实战:文件级读写锁(完整脚本)

以下脚本用于保护/etc/app/config.cfg文件,支持多进程读,单进程写:

#!/bin/bash
CONFIG_FILE="/etc/app/config.cfg"
LOCK_FILE="/tmp/config.lock"
safe_read() {
    exec 50>$LOCK_FILE
    flock -s 50
    cat "$CONFIG_FILE"
    flock -u 50
}
safe_write() {
    exec 51>$LOCK_FILE
    flock -x 51
    echo "$1" > "$CONFIG_FILE"
    flock -u 51
}
# 模拟并发:10个读进程 + 1个写进程
for i in {1..10}; do
    (safe_read) &
done
(safe_write "new_value=100") &
wait

常见问题与避坑指南(问答形式)

❓ Q1:使用flock时,锁文件需要提前创建吗?

答案:不需要。exec 50>$LOCK_FILE会自动创建文件(若不存在),但如果目录权限不足会报错,建议放在/tmp或专用目录。

❓ Q2:读锁和写锁如果用同一个文件描述符,会怎样?

⚠️ 答案:混乱!务必使用不同的文件描述符(如200用于读,201用于写),否则同一进程先用读锁后来又申请写锁,可能死锁。

❓ Q3:如何防止写锁饿死(长时间获取不到锁)?

🛠️ 方案:给读锁添加超时,例如flock -w 5表示等待5秒未获取到锁则失败,并释放资源。

if ! flock -w 5 -x 200; then
    echo "获取写锁超时"
    exit 1
fi

❓ Q4:多个脚本不共享LOCK_FILE路径怎么办?

🔗 答案:所有脚本必须约定锁文件路径,最好通过环境变量统一,例如export RW_LOCK="/var/lock/app.lock"

❓ Q5:进程异常崩溃,锁会残留吗?

🔒 答案flock内核级锁,当持有锁的进程文件描述符关闭或被杀死,内核自动释放锁,放心。


性能优化与安全建议

建议 说明
使用RAM锁文件 将锁文件放在/dev/shm/(内存文件系统),避免磁盘I/O瓶颈
减少锁粒度 不要锁定整个文件,改为锁定记录行(可通过行级标记实现)
读操作优先 尽量使用共享锁,写操作前预告(例如先创建.writepending标记)
避免重入 同一进程不要重复获取锁,否则可导致死锁
监控锁状态 定期检查锁文件数量,异常时报警

Shell中的读写锁同步并非难事,核心在于利用系统flock的共享/独占模式或自己构建原子目录锁。生产环境强烈推荐flock方法,它简洁、健壮、自动回收,理解“读读并行、读写互斥”的原则,你就能像管理数据库事务一样管理Shell脚本的并发安全。

更多服务器脚本优化技巧,可参考 shell-sync-patterns(请自行验证)。

(全文约1150字)

抱歉,评论功能暂时关闭!