按 Enter 键跳转到正文

Redis分布式锁

Redis分布式锁

一、 Redis锁基础概念

1. 为什么需要Redis锁

Redis锁是一种基于内存数据库Redis实现的分布式锁机制,主要解决分布式系统中的资源竞争问题。相比数据库锁,Redis锁具有以下优势

  • 高性能:基于内存操作,响应速度快
  • 原子性保证:Redis单线程模型天然支持原子操作
  • 分布式支持:可跨多台服务器使用
  • 丰富的数据结构:支持多种锁实现方式

2. 基本实现原理

最简单的Redis锁实现方式:

SET resource_name my_random_value NX PX 30000
  • NX:仅当key不存在时设置
  • PX:设置过期时间(毫秒)
  • my_random_value:唯一标识,用于安全释放锁

二、Redis锁的实现方式

1. SETNX实现(基本方式)

实现步骤:

  1. 尝试获取锁:SETNX lock_key 1
  2. 获取成功则设置过期时间:EXPIRE lock_key 30
  3. 执行业务逻辑
  4. 释放锁:DEL lock_key

问题: 非原子操作,SETNX和EXPIRE之间可能崩溃导致死锁

2. SET扩展参数实现(推荐)

Redis 2.6.12+版本支持扩展参数,可原子性完成设置

SET lock_key unique_value NX PX 30000

PHP实现示例:

 1$redis = new Redis();
 2$redis->connect('127.0.0.1', 6379);
 3
 4$lockKey = 'order_lock_123';
 5$uniqueId = uniqid();
 6$expire = 30000; // 30秒
 7
 8// 尝试获取锁
 9$acquired = $redis->set($lockKey, $uniqueId, ['NX', 'PX' => $expire]);
10
11if ($acquired) {
12    try {
13        // 执行业务逻辑
14        processOrder();
15    } finally {
16        // 使用Lua脚本保证原子性释放
17        $script = "
18            if redis.call('get', KEYS[1]) == ARGV[1] then
19                return redis.call('del', KEYS[1])
20            else
21                return 0
22            end
23        ";
24        $redis->eval($script, [$lockKey, $uniqueId], 1);
25    }
26} else {
27    // 获取锁失败
28    handleLockFailure();
29}

3. RedLock算法(分布式环境)

Redis官方推荐的分布式锁算法,适用于多Redis节点场景:

实现步骤:

  1. 获取当前时间(毫秒)
  2. 依次尝试从N个Redis节点获取锁
  3. 计算获取锁总耗时,当且仅当从多数节点获取成功且耗时小于锁有效期时才算成功
  4. 锁的实际有效时间 = 初始有效时间 - 获取锁耗时
  5. 释放锁时向所有节点发送释放请求

PHP实现:

 1use RedLock\RedLock;
 2
 3$servers = [
 4    ['127.0.0.1', 6379, 0.01],
 5    ['127.0.0.1', 6380, 0.01],
 6    ['127.0.0.1', 6381, 0.01]
 7];
 8
 9$redLock = new RedLock($servers);
10
11$lock = $redLock->lock('resource_name', 1000);
12
13if ($lock) {
14    try {
15        // 执行业务逻辑
16    } finally {
17        $redLock->unlock($lock);
18    }
19}

三、Redis锁的关键特性

1. 互斥性

同一时刻只有一个客户端能持有锁,通过Redis的单线程模型和NX参数保证。

2. 避免死锁

通过过期时间保证即使客户端崩溃锁也能自动释放:

SET lock_key unique_value NX PX 30000

3. 释放安全性

只有锁的持有者才能释放锁,通过Lua脚本保证原子性:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

4. 容错性

RedLock算法能在部分Redis节点宕机时继续工作,只要多数节点存活。

四、Redis锁的应用场景

1. 秒杀系统实现

 1function seckill($productId, $userId) {
 2    $redis = new Redis();
 3    $lockKey = "seckill:lock:$productId";
 4    $uniqueId = uniqid();
 5    $expire = 1000; // 1秒
 6    
 7    // 尝试获取锁
 8    $locked = $redis->set($lockKey, $uniqueId, ['NX', 'PX' => $expire]);
 9    
10    if (!$locked) {
11        return ['success' => false, 'message' => '系统繁忙,请重试'];
12    }
13    
14    try {
15        // 检查库存
16        $stockKey = "seckill:stock:$productId";
17        $stock = $redis->get($stockKey);
18        
19        if ($stock <= 0) {
20            return ['success' => false, 'message' => '已售罄'];
21        }
22        
23        // 扣减库存
24        $redis->decr($stockKey);
25        
26        // 创建订单
27        createOrder($productId, $userId);
28        
29        return ['success' => true, 'message' => '秒杀成功'];
30    } finally {
31        // 释放锁
32        $script = "if redis.call('get', KEYS[1]) == ARGV[1] then 
33                     return redis.call('del', KEYS[1]) 
34                   else 
35                     return 0 
36                   end";
37        $redis->eval($script, [$lockKey, $uniqueId], 1);
38    }
39}

2. 分布式定时任务

 1function runDistributedCron() {
 2    $redis = new Redis();
 3    $lockKey = "cron:report_generation";
 4    $uniqueId = gethostname().'-'.getmypid();
 5    $expire = 3600000; // 1小时
 6    
 7    // 尝试获取锁
 8    $locked = $redis->set($lockKey, $uniqueId, ['NX', 'PX' => $expire]);
 9    
10    if ($locked) {
11        try {
12            generateDailyReport();
13        } finally {
14            $script = "if redis.call('get', KEYS[1]) == ARGV[1] then 
15                         return redis.call('del', KEYS[1]) 
16                       end";
17            $redis->eval($script, [$lockKey, $uniqueId], 1);
18        }
19    }
20}

3. 防止重复提交

 1function submitOrder($orderData) {
 2    $redis = new Redis();
 3    $lockKey = "order:submit:".md5(json_encode($orderData));
 4    $uniqueId = session_id();
 5    $expire = 5000; // 5秒
 6    
 7    if ($redis->set($lockKey, $uniqueId, ['NX', 'PX' => $expire])) {
 8        try {
 9            return processOrder($orderData);
10        } finally {
11            $script = "if redis.call('get', KEYS[1]) == ARGV[1] then 
12                         return redis.call('del', KEYS[1]) 
13                       end";
14            $redis->eval($script, [$lockKey, $uniqueId], 1);
15        }
16    } else {
17        throw new Exception('请勿重复提交订单');
18    }
19}

五、Redis锁的最佳实践

1. 锁的粒度控制

  • 不要太粗:避免大范围锁导致性能下降
    1// 不好 - 锁整个系统
    2$lockKey = "global_order_lock";
    3
    4// 好 - 按订单ID锁定
    5$lockKey = "order_lock_".$orderId;
    
  • 不要太细:避免过多锁增加系统复杂度

2. 锁的过期时间设置

  • 根据业务操作合理设置:
    1// 简单操作 - 短时间
    2$expire = 1000; // 1秒
    3
    4// 复杂操作 - 适当延长
    5$expire = 30000; // 30秒
    
  • 设置自动续期机制(看门狗):
 1function keepAlive($redis, $lockKey, $uniqueId, $expire) {
 2    $script = "if redis.call('get', KEYS[1]) == ARGV[1] then 
 3                 return redis.call('pexpire', KEYS[1], ARGV[2]) 
 4               else 
 5                 return 0 
 6               end";
 7    return $redis->eval($script, [$lockKey, $uniqueId, $expire], 1);
 8}
 9
10// 每10秒续期一次
11Swoole\Timer::tick(10000, function() use ($redis, $lockKey, $uniqueId, $expire) {
12    keepAlive($redis, $lockKey, $uniqueId, $expire);
13});

3. 异常处理与重试机制

 1function acquireLockWithRetry($redis, $lockKey, $uniqueId, $expire, $maxRetries = 3) {
 2    $retries = 0;
 3    while ($retries < $maxRetries) {
 4        if ($redis->set($lockKey, $uniqueId, ['NX', 'PX' => $expire])) {
 5            return true;
 6        }
 7        
 8        $retries++;
 9        usleep(rand(100000, 500000)); // 随机等待0.1-0.5秒
10    }
11    return false;
12}

六、Redis锁的常见问题与解决方案

1. 锁提前过期问题

问题:业务执行时间超过锁过期时间,导致其他客户端获取锁 解决方案:

  • 合理评估业务执行时间,设置足够长的过期时间
  • 实现锁续期机制(看门狗)

2. 时钟漂移问题

**问题:**多台服务器时钟不一致影响RedLock算法 解决方案:

  • 使用NTP服务同步时钟
  • 适当延长锁的有效期

3. 客户端阻塞问题

**问题:**客户端长时间GC停顿导致锁失效 解决方案:

  • 监控GC情况,优化应用内存使用
  • 缩短锁的有效期,增加续期频率

七、总结

Redis分布式锁是构建高并发分布式系统的重要组件,正确使用需要注意:

  1. 使用SET NX PX命令原子性获取锁
  2. 为每个锁设置唯一的随机值
  3. 使用Lua脚本原子性释放锁
  4. 合理设置锁的粒度和过期时间
  5. 在分布式环境考虑使用RedLock算法
  6. 实现适当的重试和异常处理机制

Redis锁虽然强大,但并非万能,对于需要强一致性的场景,可能需要结合数据库事务或其他分布式协调服务(如ZooKeeper)来实现。

发表评论