作为经历过618大促奋战的牛马,我曾亲眼见过分布式锁没处理好导致的百万级资损。今天咱们来聊聊Redis分布式锁,最后做成注解工具,方便使用!
一、先看个案例
场景:社区团购秒杀芒果,库存1000件,结果卖出1200件
代码:
public void reduceStock(String productId) {
// 查库存
int stock = getStock(productId);
if(stock > 0){
// 扣库存
updateStock(productId, stock-1);
}
}
问题:当100个请求同时查到库存=1时,全部执行扣减,导致超卖200件!
二、分布式锁的四大金刚
1. 互斥性(最基本)
- 同一时刻只能有一个客户端持有锁
2. 防死锁(必须做到)
// 错误示范(没有超时时间)
redis.set("lock", "1", "NX");
// 正确姿势(原子操作设置过期时间)
redis.set("lock", "1", "NX", "EX", 30);
3. 谁加的锁谁解(关键细节)
// 错误示范(直接删除)
redis.del("lock");
// 正确姿势(校验身份)
if(redis.get("lock").equals(myId)){
redis.del("lock");
}
4. 高可用(生产必备)
- 单节点Redis宕机怎么办?
- 集群方案:Redis Sentinel 或 Cluster
三、redis锁代码(带自动续命)
public class RedisLock {
private final JedisPool jedisPool;
private final String lockKey;
private final String clientId = UUID.randomUUID().toString();
private volatile boolean isLocked = false;
private ScheduledExecutorService renewExecutor;
public RedisLock(JedisPool jedisPool, String lockKey) {
this.jedisPool = jedisPool;
this.lockKey = lockKey;
}
/**
* 加锁(支持超时和重试)
*/
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
long start = System.currentTimeMillis();
long maxWaitMillis = unit.toMillis(waitTime);
while (System.currentTimeMillis() - start < maxWaitMillis) {
try (Jedis jedis = jedisPool.getResource()) {
// 核心加锁命令
String result = jedis.set(lockKey, clientId, SetParams.setParams().nx().ex(30));
if ("OK".equals(result)) {
isLocked = true;
startRenewal(); // 启动续命线程
return true;
}
}
// 随机等待避免惊群效应
Thread.sleep(50 + new Random().nextInt(50));
}
return false;
}
/**
* 自动续期(保活机制)
*/
private void startRenewal() {
renewExecutor = Executors.newSingleThreadScheduledExecutor();
renewExecutor.scheduleAtFixedRate(() -> {
try (Jedis jedis = jedisPool.getResource()) {
if (isLocked && clientId.equals(jedis.get(lockKey))) {
jedis.expire(lockKey, 30); // 续期30秒
}
}
}, 10, 10, TimeUnit.SECONDS); // 每10秒检查一次
}
/**
* 解锁(Lua脚本保证原子性)
*/
public void unlock() {
if (!isLocked) return;
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
try (Jedis jedis = jedisPool.getResource()) {
Object result = jedis.eval(script,
Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (result.equals(1L)) {
stopRenewal();
isLocked = false;
}
}
}
private void stopRenewal() {
if (renewExecutor != null) {
renewExecutor.shutdown();
}
}
}
四、代码提问
1. 加锁为什么要用UUID?
- 防止其他客户端误删锁(比如A客户端超时后,B获取锁,A这时不能删B的锁)
2. 自动续期有什么用?
- 解决业务执行时间超过锁过期时间的问题
- 示例:设置30秒过期,每10秒续期一次
3. 为什么要用Lua脚本解锁?
- 保证
判断持有者
和删除锁
的原子性 - 防止执行到中间步骤时锁过期
五、测试大法
场景1:并发抢购测试
@Test
void testConcurrentLock() throws InterruptedException {
int threadCount = 100;
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
AtomicInteger successCount = new AtomicInteger();
for (int i = 0; i < threadCount; i++) {
pool.submit(() -> {
try (Jedis jedis = jedisPool.getResource()) {
RedisLock lock = new RedisLock(jedisPool, "test_lock");
if (lock.tryLock(3, TimeUnit.SECONDS)) {
successCount.incrementAndGet();
Thread.sleep(100); // 模拟业务处理
lock.unlock();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
assertEquals(1, successCount.get());
}
场景2:网络闪断测试
@Test
void testNetworkJitter() {
RedisLock lock = new RedisLock(jedisPool, "network_test");
// 模拟获取锁后Redis宕机
lock.tryLock(1, TimeUnit.SECONDS);
jedisPool.getResource().shutdown(); // 强制断开连接
// 业务处理中...
assertThrows(Exception.class, () -> {
lock.unlock(); // 应该抛出连接异常
});
}
六、注解式分布式锁工具
1. 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisLockable {
String key(); // 锁的key
int waitTime() default 3; // 等待时间(秒)
int leaseTime() default 30; // 持有时间(秒)
}
2. AOP切面实现
@Aspect
@Component
public class RedisLockAspect {
@Autowired
private JedisPool jedisPool;
@Around("@annotation(lockable)")
public Object around(ProceedingJoinPoint joinPoint, RedisLockable lockable) throws Throwable {
String lockKey = parseKey(joinPoint, lockable.key());
RedisLock lock = new RedisLock(jedisPool, lockKey);
try {
if (!lock.tryLock(lockable.waitTime(), TimeUnit.SECONDS)) {
throw new RuntimeException("获取锁失败");
}
return joinPoint.proceed();
} finally {
lock.unlock();
}
}
// 解析SpEL表达式(支持动态key)
private String parseKey(ProceedingJoinPoint joinPoint, String keyExpr) {
// 实现略,可用Spring Expression解析
return keyExpr;
}
}
3. 使用示例
@Service
public class OrderService {
@RedisLockable(key = "'order_lock:' + #orderId",
waitTime = 5,
leaseTime = 60)
public void createOrder(String orderId) {
// 业务逻辑
}
}
七、踩坑预警
坑1:锁提前过期导致数据混乱
症状:日志显示两个线程同时进入临界区
解法:
- 合理设置leaseTime(建议业务耗时的2-3倍)
- 启用健康检查自动续期
坑2:解锁失败导致死锁
症状:监控显示锁TTL一直刷新
解法:
// 在finally中强制解锁(慎用!)
finally {
if(lock.isLocked()){
log.warn("强制解锁");
lock.forceUnlock();
}
}
坑3:集群脑裂导致双主
症状:两个节点同时持有锁
解法:
- 使用Redlock算法(需要至少3个主节点)
- 设置
min-slaves-to-write 1
(Redis配置)
八、结 Checklist
当你实现分布式锁时,请对照检查:
- 是否设置唯一客户端标识
- 是否具备自动续期机制
- 解锁操作是否原子性
- 是否处理了网络分区问题
- 是否有完善的监控(获取次数、等待时间等)