Published on

Redis锁

Authors
  • avatar
    Name
    Shelton Ma
    Twitter

1. Redis 锁的使用场景

在高并发环境中,Redis 锁可以有效防止数据不一致、超卖、重复执行等问题.而在复杂的业务逻辑中,重入锁进一步解决了递归调用和多层业务中对锁的重复获取问题.

2. redis锁实现

const Redis = require('ioredis');
const redis = new Redis(); // 默认连接到 localhost:6379

// 尝试获取分布式锁
async function acquireLock(lockKey, value, ttl) {
  const result = await redis.set(lockKey, value, 'NX', 'PX', ttl); 
  // NX: 键不存在时设置,PX: 设置过期时间(毫秒)
  return result === 'OK'; // 如果返回 OK,表示锁获取成功
}

// 释放分布式锁
async function releaseLock(lockKey, value) {
  // 使用 Lua 脚本确保只有锁的持有者才能释放锁
  const luaScript = `
    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    else
      return 0
    end
  `;
  const result = await redis.eval(luaScript, 1, lockKey, value);
  return result === 1; // 如果返回 1,表示锁已被释放
}

// 测试获取和释放锁
async function testLock() {
  const lockKey = 'my_lock_key';
  const lockValue = 'unique_lock_value';
  const ttl = 10000; // 锁的过期时间(毫秒)

  const lockAcquired = await acquireLock(lockKey, lockValue, ttl);
  if (lockAcquired) {
    console.log('Lock acquired!');

    // 模拟业务操作
    setTimeout(async () => {
      const released = await releaseLock(lockKey, lockValue);
      if (released) {
        console.log('Lock released!');
      } else {
        console.log('Failed to release lock.');
      }
    }, 5000); // 假设业务操作需要 5 秒钟
  } else {
    console.log('Failed to acquire lock.');
  }
}

testLock();

3. redis 红锁实现

RedLock 是 Redis 官方推荐的算法,通过多个 Redis 实例来增加分布式锁的可靠性.ioredis 库并没有内建 RedLock,但你可以使用像 Redlock.js 这样的库来简化实现.

const Redis = require('ioredis');
const Redlock = require('redlock');

const redis = new Redis();
const redlock = new Redlock([redis], {
  driftFactor: 0.01, // 锁时间漂移因子
  retryCount: 10, // 最大重试次数
  retryDelay: 200, // 重试间隔(毫秒)
  retryJitter: 200 // 重试时的随机抖动(毫秒)
});

// 获取锁
async function acquireRedLock(lockKey) {
  try {
    const lock = await redlock.lock(lockKey, 10000); // 获取 10 秒的锁
    console.log('Lock acquired:', lock);
    return lock;
  } catch (err) {
    console.error('Failed to acquire lock:', err);
  }
}

// 释放锁
async function releaseRedLock(lock) {
  try {
    await lock.unlock();
    console.log('Lock released!');
  } catch (err) {
    console.error('Failed to release lock:', err);
  }
}

// 测试
async function testRedLock() {
  const lockKey = 'my_red_lock';
  const lock = await acquireRedLock(lockKey);
  if (lock) {
    setTimeout(async () => {
      await releaseRedLock(lock);
    }, 5000); // 模拟5秒后释放锁
  }
}

testRedLock();

4. 重入锁使用场景

重入锁(Reentrant Lock)允许同一线程在持有锁的情况下,再次获取该锁.这种机制通常用于递归调用、复杂业务流程中的多次锁定.

1. 重入锁实现

const Redis = require('ioredis');
const redis = new Redis(); // 默认连接到 localhost:6379

// 锁的过期时间(毫秒)
const lockTTL = 10000; // 10秒

// 获取唯一的锁标识(客户端标识)
function generateLockValue() {
  return `lock_${Math.random().toString(36).substr(2, 9)}`;
}

// 尝试获取分布式可重入锁
async function acquireReentrantLock(lockKey, clientId) {
  const existingLockValue = await redis.get(lockKey);
  
  if (!existingLockValue) {
    // 锁不存在,设置锁
    const result = await redis.set(lockKey, `${clientId}:1`, 'NX', 'PX', lockTTL);
    return result === 'OK';
  } else {
    // 锁已经存在,检查是否是同一个客户端
    const [owner, count] = existingLockValue.split(':');
    if (owner === clientId) {
      // 同一个客户端,重入锁,递增计数器
      const newCount = parseInt(count, 10) + 1;
      await redis.set(lockKey, `${clientId}:${newCount}`, 'PX', lockTTL); // 延长过期时间
      return true;
    }
    // 如果不是同一个客户端,无法获取锁
    return false;
  }
}

// 释放分布式可重入锁
async function releaseReentrantLock(lockKey, clientId) {
  const existingLockValue = await redis.get(lockKey);
  
  if (!existingLockValue) {
    // 锁不存在,不需要释放
    return false;
  }
  
  const [owner, count] = existingLockValue.split(':');
  
  if (owner === clientId) {
    const newCount = parseInt(count, 10) - 1;
    
    if (newCount > 0) {
      // 如果还有重入次数,更新计数器
      await redis.set(lockKey, `${clientId}:${newCount}`, 'PX', lockTTL);
    } else {
      // 重入次数为 0,释放锁
      await redis.del(lockKey);
    }
    return true;
  }
  
  // 如果不是同一个客户端,不能释放锁
  return false;
}

// 测试可重入锁
async function testReentrantLock() {
  const lockKey = 'my_reentrant_lock';
  const clientId = generateLockValue();

  const lockAcquired = await acquireReentrantLock(lockKey, clientId);
  console.log('Lock acquired:', lockAcquired);

  if (lockAcquired) {
    // 模拟业务操作
    console.log('First operation...');
    
    // 重入操作
    const reentered = await acquireReentrantLock(lockKey, clientId);
    console.log('Reentered lock:', reentered);

    if (reentered) {
      console.log('Second operation...');

      // 释放锁
      await releaseReentrantLock(lockKey, clientId);
      console.log('Released once...');
      
      // 释放锁
      await releaseReentrantLock(lockKey, clientId);
      console.log('Released completely...');
    }
  }
}

testReentrantLock().catch(console.error);

5. Redis 锁的最佳实践

  • 使用 EX (expire) 设置锁过期时间,防止死锁
  • 确保锁的 key 唯一,避免不同场景产生冲突
  • 尽量采用 Redlock 以提升稳定性,防止分布式锁失败
  • 确保释放锁逻辑在 finally 块内,防止异常导致锁未释放
  • 避免锁的粒度过大,尽可能缩短锁的持有时间,减少阻塞

6. 常见问题解答

  1. 为什么要使用 NX (Not Exists)NX 确保仅在锁不存在时才加锁,防止并发请求时的锁覆盖.

  2. 为什么需要 EX 过期时间? 防止意外崩溃导致锁未释放,产生死锁.

  3. Redlock 什么时候优于传统锁? 在多实例部署、复杂业务逻辑、需支持重入的场景下,Redlock 更稳定可靠.