单机环境下的锁用synchronized就够了。但分布式环境下,多个服务实例共享同一份数据,锁就得靠Redis来实现。
今天把Redis分布式锁的三种实现方案都过一遍,包括每种方案的坑。
方案一:SETNX(最基础)
java
// 加锁
public boolean tryLock(String key, String value, long expireSeconds) {
// SETNX = SET if Not eXists
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
// 解锁
public void unlock(String key, String value) {
// 先判断是不是自己的锁,再删除
String currentValue = redisTemplate.opsForValue().get(key);
if (value.equals(currentValue)) {
redisTemplate.delete(key);
}
}
坑1:判断和删除不是原子操作
css
线程A:get → 是我的锁
此时锁过期了
线程B:set成功,拿到了锁
线程A:delete → 把B的锁删了!
解决:用Lua脚本保证原子性。
java
// Lua脚本:原子性地判断+删除
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
lockValue
);
坑2:锁过期了但业务还没执行完
解决:锁续期。但自己实现续期很复杂,所以有了Redisson。
方案二:Redisson(推荐)
Redisson是Java生态中最成熟的Redis客户端,内置了分布式锁实现。
java
// 引入依赖
// <dependency>
// <groupId>org.redisson</groupId>
// <artifactId>redisson-spring-boot-starter</artifactId>
// <version>3.25.0</version>
// </dependency>
// 配置
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("yourpassword");
return Redisson.create(config);
}
}
// 使用
@Service
public class OrderService {
@Autowired
private RedissonClient redisson;
public void createOrder(Long userId) {
RLock lock = redisson.getLock("order:" + userId);
try {
// 加锁:等待3秒,锁定后30秒自动释放
boolean acquired = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (!acquired) {
throw new BusinessException("系统繁忙,请稍后重试");
}
// 执行业务逻辑
processOrder(userId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 解锁(只有持有锁的线程才能解锁)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Redisson的看门狗机制自动续期:
erlang
加锁成功(默认30s过期)
↓
业务执行中...
↓
每10s自动续期一次(看门狗线程)
↓
业务执行完毕,主动解锁
↓
如果服务宕机,看门狗停止,30s后锁自动过期
方案三:RedLock(多节点)
单节点Redis的分布式锁有隐患------如果Redis主节点挂了,锁数据还没同步到从节点,其他线程可能重复加锁。
RedLock的思路:在多个独立的Redis节点上加锁,大多数节点加锁成功才算成功。
java
// 多个独立Redis实例
List<RedisURI> uris = Arrays.asList(
new RedisURI("redis://node1:6379"),
new RedisURI("redis://node2:6379"),
new RedisURI("redis://node3:6379")
);
Config config = new Config();
// 配置多节点...
RedissonRedLock redLock = redisson.getRedLock(
redisson.getLock("lock1"),
redisson.getLock("lock2"),
redisson.getLock("lock3")
);
redLock.lock(); // 至少N/2+1个节点加锁成功
但RedLock争议很大------Martin Kleppmann(《DERTA》作者)公开质疑过其安全性。实际项目中,大部分场景用Redisson单节点就够了。
三种方案对比
| 对比 | SETNX | Redisson | RedLock |
|---|---|---|---|
| 实现难度 | 低(但坑多) | 低(封装好了) | 高 |
| 安全性 | 中(有坑) | 高(看门狗续期) | 最高 |
| 性能 | 最好 | 好 | 较差 |
| 推荐度 | 学习用 | 生产推荐 | 极端场景 |
使用注意事项
| 注意点 | 说明 |
|---|---|
| 设置过期时间 | 必须设!否则死锁 |
| 唯一标识 | value用UUID+线程ID,防止误删别人的锁 |
| 重试策略 | tryLock失败后重试,别直接报错 |
| 锁粒度 | 越细越好,别锁太大 |
| 超时时间 | 比业务最大执行时间稍长 |
分布式锁是微服务架构中的常见需求。Redisson的实现已经足够成熟稳定,大部分场景直接用它就好。
你们项目里分布式锁用的什么方案?