最近在群里看到一个哥们说:“工作5年了,简历上写了熟悉分布式系统,结果一次分布式锁都没用过。是我太菜?还是公司系统太稳了?”,底下立马一堆消息。 “+1,我也是!” “别说用了,我们连分布俩字都少见。” “你们还好,我们公司连Redis集群都没有,就一台单机Redis。”
嘿嘿,我认为这应该是大部分开发的日常吧?下面我也来整理一下我理解的点吧!
1. 啥是分布式锁?
当多个服务抢同一个资源时,需要定个规矩:谁先来的谁先用。
比如:
- 两个服务器同时给用户发红包,不能重发。
- 秒杀活动,库存就10个,不能超卖。
- 定时任务在多个节点上不能重复跑。
这些,才需要分布式锁。可现实是很多公司,压根没到那个量级。
2. 实际场景
举两个常见的例子,看大家有遇到过没:
场景一:超卖问题
某次大促,你们搞了一个“限量100件”的秒杀活动。结果活动结束,卖了130件,仓库傻了,客服也炸了。
原因就是:十台服务器同时卖货,每个人都检测到“还有库存”,于是都成功下单。
这时候就要用分布式锁,在真正减库存之前,先抢锁,抢到了才能减。
场景二:重复退款
用户点退款,手抖连续点了两次,你的服务部署了三个节点,两个节点都接收到了请求,结果退了两次钱。
虽然最后能追回,但财务可能要提着刀来找你了。
3. Redis分布式锁实现
下面是一个基于SpringBoot的简单实现:
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 尝试获取分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识(用于释放锁时验证)
* @param expireTime 锁的过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String requestId, long expireTime) {
return redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS);
}
/**
* 释放分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识
*/
public void releaseLock(String lockKey, String requestId) {
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (requestId.equals(currentValue)) {
redisTemplate.delete(lockKey);
}
}
}
// 使用示例
@Service
public class OrderService {
@Autowired
private RedisDistributedLock distributedLock;
public void createOrder(String productId, int quantity) {
String lockKey = "lock:order:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁,等待3秒,锁过期时间为10秒
boolean locked = distributedLock.tryLock(lockKey, requestId, 10);
if (!locked) {
throw new RuntimeException("系统繁忙,请重试");
}
// 到这里说明拿到锁了,执行核心业务逻辑
checkStock(productId); // 检查库存
reduceStock(productId, quantity); // 扣减库存
createOrderRecord(productId, quantity); // 创建订单
} finally {
// 无论如何都要释放锁
distributedLock.releaseLock(lockKey, requestId);
}
}
private void checkStock(String productId) {
// 检查库存逻辑
}
private void reduceStock(String productId, int quantity) {
// 扣减库存逻辑
}
private void createOrderRecord(String productId, int quantity) {
// 创建订单逻辑
}
}
但这只是个基础版,真上生产环境还得考虑下面这几个点:
- 锁过期时间太短:业务没执行完,锁没了,别人进来了
- 锁过期时间太长:业务挂了,锁一直不释放,其他人傻等
- 释放了别人的锁:自己阻塞久了,锁过期被别人抢了,然后又释放了别人的
所以实际项目中建议直接用现成的库,比如Redisson,别自己瞎造轮子。
4. Redisson分布式锁
Redisson
已经帮我们解决了上面的所有问题,使用起来超级简单:
Maven
加依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.1</version>
</dependency>
业务逻辑
@Service
public class PaymentService {
@Autowired
private RedissonClient redissonClient;
public void processRefund(String orderNo) {
String lockKey = "lock:refund:" + orderNo;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待100秒,上锁10秒后自动解锁
boolean isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (isLocked) {
// 执行退款核心逻辑
if (hasRefunded(orderNo)) {
throw new RuntimeException("该订单已退款,请勿重复操作");
}
doRefund(orderNo);
recordRefund(orderNo);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁失败", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private boolean hasRefunded(String orderNo) {
// 检查是否已经退款
return false;
}
private void doRefund(String orderNo) {
// 执行退款逻辑
}
private void recordRefund(String orderNo) {
// 记录退款信息
}
}
搞定,感兴趣的朋友可以试一下。
5. 小结
如果你工作几年都没碰过分布式锁,无非两种情况:
- 公司业务确实稳:用户量没那么大,根本不需要分布式部署;或者业务场景本来就不会冲突。
- 有人帮你扛了:很多框架和数据库已经内置了类似的机制,比如数据库的悲观锁(SELECT ... FOR UPDATE)、乐观锁(version字段)、Redis的原子操作(INCR/DECR)等等,你可能在不知不觉中已经避免了问题。
所以也别说自己菜,没遇到说明你们系统跑得足够顺,这是好事!
但话说回来,现在没用过不代表以后用不到。跳槽、业务发展都可能突然遇到。所以收藏一下这篇文章,真要用的时候翻出来,能救急。
分布式锁属于“平时用不着,面试老爱问”的知识点。没实际搞过不丢人,但要知道为啥要用、怎么上手。
你工作几年?用过分布式锁吗?欢迎评论区聊聊你的经历!
?往期精彩
《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》
《只会写 Mapper 就敢说会 MyBatis?面试官:原理都没懂》
《别再手写判空了!SpringBoot 自带的 20 个高效工具类》
《别学23种了!Java项目中最常用的6个设计模式,附案例》
《Vue3+TS设计模式:5个真实场景让你代码更优雅》