SpringBoot + Redis 实现分布式锁实战
前言
单体应用下,我们可以直接使用 synchronized 或者 ReentrantLock 解决并发争抢资源的问题。但随着服务集群化部署,多个服务实例运行在不同JVM进程,本地锁只能锁住当前实例,无法跨实例生效。
举个典型场景:电商库存扣减,服务部署3个实例,大量请求同时过来,本地锁会失效,出现库存超卖。分布式锁就是用来解决跨进程、跨服务并发资源竞争的方案。Redis因为高性能、操作原子性,是实现分布式锁最常用方案之一。
本文基于 SpringBoot 2.7 + RedisTemplate,从零实现Redis分布式锁,同时分析各种踩坑点。
一、环境依赖
引入maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
application.yml 配置redis连接
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
二、简易版:SETNX + 过期时间(不推荐生产)
SETNX:只有key不存在时才设置值,返回成功代表抢到锁。同时必须设置过期时间,防止服务宕机导致锁一直存在,造成死锁。
⚠️ 坑点:setnx 和 expire 如果分成两条命令,中间服务崩溃,锁不会自动过期,发生死锁。必须保证原子性。
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
public class RedisDistLock {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取锁
* @param lockKey 锁名称
* @param lockValue 唯一标识(UUID),防止释放别人的锁
* @param expireTime 过期时间,单位秒
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String lockValue, long expireTime){
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
// set key value nx ex 原子命令
Boolean success = ops.setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁:判断value一致才删除,防止锁过期释放了别人的锁
*/
public boolean unLock(String lockKey, String lockValue){
Object val = redisTemplate.opsForValue().get(lockKey);
if(lockValue.equals(val)){
return Boolean.TRUE.equals(redisTemplate.delete(lockKey));
}
return false;
}
}
问题:get + delete 两条命令,非原子。极端情况下:查询value相等之后,锁刚好过期,别的线程拿到锁,此时执行delete,就会删掉其他线程的锁。
解决方案:使用Lua脚本保证释放锁原子性
三、生产可用:Lua脚本实现分布式锁
Lua脚本在Redis中是原子执行,整个脚本执行期间不会被其他命令打断。获取锁依旧使用setIfAbsent原子命令,释放锁交给Lua脚本。
@Component
public class RedisLuaDistLock {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// Lua脚本:判断value相等才删除key
private static final String UNLOCK_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
public boolean tryLock(String lockKey, String lockValue, long expireTime){
Boolean ok = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(ok);
}
public boolean unLock(String lockKey, String lockValue){
Long result = redisTemplate.execute(
new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
Collections.singletonList(lockKey),
lockValue
);
return result != null && result == 1;
}
}
四、测试Controller
@RestController
@RequestMapping("/stock")
public class StockController {
@Resource
private RedisLuaDistLock redisLock;
// 模拟库存
private int stock = 10;
@GetMapping("/deduct")
public String deductStock(){
String lockKey = "lock:stock";
String lockValue = UUID.randomUUID().toString();
boolean getLock = redisLock.tryLock(lockKey, lockValue, 10);
if(!getLock){
return "请求繁忙,请稍后重试";
}
try {
if(stock > 0){
stock --;
return "扣减成功,剩余库存:" + stock;
}else {
return "库存不足";
}
}finally {
// 必须finally释放锁,保证锁一定释放
redisLock.unLock(lockKey, lockValue);
}
}
}
五、分布式锁核心问题分析
- 锁超时问题
业务执行时间超过锁过期时间,锁提前释放,其他线程抢到锁。解决方案:锁续期(看门狗),Redisson内部已经封装好了看门狗机制,生产优先考虑Redisson。 - 锁误删除
每个锁value使用唯一UUID,只能释放自己加的锁,不能直接delete key。 - Redis主从切换锁丢失
主节点加锁成功,但还没同步到从节点,主宕机,从晋升为主,锁丢失。高一致性场景,推荐RedLock,或者数据库方案。
六、生产选型建议
简单项目:自研Lua版本分布式锁可以临时使用。
企业级项目:不要手写轮子,直接使用 Redisson,内置可重入锁、看门狗自动续期、锁重试、RedLock等能力,成熟稳定。
七、总结
Redis分布式锁核心要点:
- 加锁:
SET key value NX EX原子操作; - 解锁:Lua脚本保证原子校验+删除;
- 锁value必须唯一,防止误删;
- 手写锁适合学习理解原理,真实业务优先Redisson。
分布式锁是后端面试高频考点,通过本篇实战代码,可以快速理解分布式并发的核心难点,同时掌握Redis原子命令与Lua脚本在项目中的落地方式。
- 点赞
- 收藏
- 关注作者
评论(0)