一、Redis分布式锁基础实现
public class RedisDistributedLock {
private JedisPool jedisPool;
private String lockKey;
private String clientId;
private int expireTime = 30; // 默认30秒
public boolean tryLock() {
try (Jedis jedis = jedisPool.getResource()) {
// NX表示不存在时设置,PX设置过期时间(毫秒)
String result = jedis.set(lockKey, clientId, SetParams.setParams().nx().px(expireTime * 1000));
return "OK".equals(result);
}
}
public void unlock() {
try (Jedis jedis = jedisPool.getResource()) {
// 使用Lua脚本保证原子性
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(clientId));
}
}
}
关键点:
- 使用SET NX PX命令保证原子性
- 单命令原子性:Redis服务器单线程顺序执行命令
- 仅包含SET操作:仅完成键值设置和过期时间配置
- 无逻辑判断:仅判断键是否存在(NX特性)
- 客户端唯一标识(clientId)防止误删
- Lua脚本保证解锁操作的原子性
- 多命令原子性:组合GET、DEL等命令的复合操作
- 包含业务逻辑:实现"比较后删除"的CAS(Compare And Set)操作
- 支持复杂流程:可包含条件判断、循环等逻辑
缺陷:
如果锁存在A redis节点,然后B是A的从库,服务先获取A节点的redis key锁,如果A网络波动的时候的时候,主从切换,B节点升级为主节点,这个时候另一个服务获取B节点的相同的redis key,这种情况就发生脑裂了。
二、RedLock算法核心思想
RedLock算法由Redis作者提出,主要解决单点故障问题:
- 多节点部署:使用5个(奇数)独立的Redis节点
- 顺序获取:客户端依次向所有节点申请锁
- 成功条件:获得超过半数的锁(3个)
- 耗时计算:总耗时应小于锁的TTL时间
- 失败释放:失败时需要释放所有已获得的锁
三、Redisson看门狗机制原理
public class RedissonWatchdogExample {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");
try {
// 默认30秒过期,看门狗自动续期
lock.lock();
// 业务逻辑执行时间可能超过30秒
Thread.sleep(40000);
} finally {
lock.unlock();
}
}
}
集群防止脑裂
Redisson 实现分布式锁的核心机制和集群脑裂防护原理如下:
1. 基础锁实现原理:
-- Redis 原子操作脚本
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
end
2. 集群模式防护机制:
// RedissonMultiLock 集群锁实现
List<RLock> locks = new ArrayList<>();
locks.add(redissonClient1.getLock("lock1"));
locks.add(redissonClient2.getLock("lock2"));
RLock multiLock = new RedissonMultiLock(locks.toArray(new RLock[0]));
关键实现要点:
1. 多节点提交机制:
- 需要 N/2+1 个节点成功获取锁才算有效
- 使用异步线程维持锁心跳(Watchdog 机制)
// Watchdog 线程实现(伪代码)
private void scheduleExpirationRenewal() {
if (expirationRenewalMap.putIfAbsent(lockName, newTimeout) == null) {
// 每 10 秒续期一次
internalLockLeaseTime / 3 周期执行
}
}
2. 脑裂防护策略:
- 同步延迟控制:主从同步超时时间 > 锁过期时间
- 多数派原则:成功获取锁的节点数 > 集群半数节点
- 故障转移阻断:当节点失联时自动启动锁失效倒计时
3. 异常处理机制:
// 锁释放时的集群同步
public void unlock() {
for (RLock lock : locks) {
if (lock.isHeldByCurrentThread()) {
lock.unlockAsync();
}
}
}
集群模式下重要参数配置建议:
# redisson.yaml
clusterServersConfig:
nodeAddresses:
- "redis://127.0.0.1:7000"
- "redis://127.0.0.1:7001"
scanInterval: 1000
retryAttempts: 3
retryInterval: 500
slaveConnectionMinimumIdleSize: 8
failedSlaveReconnectionInterval: 30000
该实现通过以下方式保证脑裂场景下的数据一致性:
- 使用 Raft 式多数派提交协议
- 网络分区时自动降级为只读模式
- 主节点切换后新主节点会等待旧主节点锁超时
- 客户端自动检测集群拓扑变化并重建连接
看门狗机制关键点: - 后台线程:每10秒检查锁状态
- 自动续期:当业务未完成时,将过期时间重置为30秒
- 客户端存活判断:只有客户端保持活跃才会续期
- 默认配置:lockWatchdogTimeout=30秒
四、完整方案对比
方案 | 优点 | 缺点 |
---|---|---|
基础Redis锁 | 实现简单 | 单点故障风险 |
RedLock算法 | 高可用性 | 实现复杂、性能损耗 |
Redisson实现 | 自动续期、可重入锁、多种锁类型 | 需要维护客户端连接 |
实际建议:
- 单节点场景使用Redisson基础锁
- 高可用场景使用Redisson+Redis Cluster
- 极端可靠性需求使用RedLock算法
生产环境注意事项:
- 合理设置超时时间(业务平均耗时 * 2)
- 监控锁等待时间和获取次数
- 为不同业务使用不同的锁前缀
- 做好锁等待超时的异常处理
竞品分析
分布式锁实现方案对比及优劣势分析
一、ZooKeeper 实现方案
// 使用Curator框架示例
public class ZkDistributedLock {
private CuratorFramework client;
private InterProcessMutex lock;
public boolean tryLock(String lockPath) throws Exception {
lock = new InterProcessMutex(client, lockPath);
return lock.acquire(3, TimeUnit.SECONDS); // 3秒获取超时
}
public void unlock() throws Exception {
if (lock != null) {
lock.release();
}
}
}
核心原理:
通过创建临时顺序节点实现,获取锁的客户端会生成有序节点,只有序号最小的节点持有锁
优势:
- 自动释放(会话失效时自动删除节点)
- 公平锁机制(顺序节点)
- 强一致性保证
劣势:
- 写操作性能低于Redis
- 需要维护ZooKeeper集群
- 客户端实现相对复杂
二、Etcd 实现方案
// 使用jetcd客户端示例
public class EtcdDistributedLock {
private Client client;
private Lease lease;
public boolean tryLock(String lockKey) throws Exception {
lease = client.getLeaseClient().grant(30).get(); // 30秒租约
Txn txn = client.getKVClient().txn();
txn.If(new Cmp(lockKey, Cmp.Op.EQUAL, CmpTarget.version(0)))
.Then(Op.put(lockKey, "locked", PutOption.newBuilder().withLeaseId(lease.getID()).build()))
.Else(Op.get(lockKey));
return txn.commit().get().isSucceeded();
}
}
核心原理:
基于租约(Lease)机制,利用事务操作实现原子性锁获取
优势:
- 强一致性(Raft协议)
- 自动续期机制
- 支持公平锁/非公平锁
劣势:
- 运维复杂度较高
- 客户端生态不如Redis完善
- 性能低于Redis
三、数据库实现方案
// 基于MySQL的乐观锁实现
public class DbDistributedLock {
@Transactional
public boolean tryLock(String lockName) {
// 使用唯一索引约束
int result = jdbcTemplate.update(
"INSERT INTO distributed_lock(lock_name,owner) VALUES (?,?) ON DUPLICATE KEY UPDATE owner=IF(expire_time < NOW(), VALUES(owner), owner)",
lockName, UUID.randomUUID().toString());
return result > 0;
}
}
核心原理:
基于数据库唯一约束或排他锁(SELECT FOR UPDATE)
优势:
- 无需额外中间件
- 实现简单快速
劣势:
- 性能差(高并发场景容易成为瓶颈)
- 无自动释放机制
- 死锁风险较高
方案对比总结表
方案 | 一致性 | 性能 | 自动释放 | 实现复杂度 | 适用场景 |
---|---|---|---|---|---|
Redis | 最终一致 | 高 | 支持 | 简单 | 高并发、允许短暂不一致 |
ZooKeeper | 强一致 | 中 | 支持 | 复杂 | 强一致性要求、公平锁场景 |
Etcd | 强一致 | 中 | 支持 | 较复杂 | 强一致性且需要自动续期 |
数据库 | 强一致 | 低 | 不支持 | 简单 | 低频访问、无中间件环境的应急方案 |
选型建议:
- 追求性能 ➜ Redis(Redisson实现)
- 强一致性要求 ➜ ZooKeeper/Etcd
- 无中间件环境 ➜ 数据库方案(需谨慎处理超时)
- 混合使用场景 ➜ 可组合使用(如Redis做主锁,数据库做备用锁)