- Published on
Redis锁
- Authors
- Name
- Shelton Ma
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. 常见问题解答
为什么要使用
NX (Not Exists)
?NX
确保仅在锁不存在时才加锁,防止并发请求时的锁覆盖.为什么需要
EX
过期时间? 防止意外崩溃导致锁未释放,产生死锁.Redlock 什么时候优于传统锁? 在多实例部署、复杂业务逻辑、需支持重入的场景下,Redlock 更稳定可靠.