JAVA-实战8 Redis实战项目—雷神点评(4)分布式锁
小鳥の翼がついに大きくなって
分布式锁
集群下的线程并发安全问题
在集群环境下,每台机器对应一个JVM,都存在一个独立的锁监视器,而常规的线程锁synchronized只能保证一个JVM的多线程安全,在集群环境下下无法生效

基本原理和实现方式
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
分布式锁的特点:
多进程可见
互斥
高性能
高可用
安全性
分布式锁示意图如下:

分布式锁常见实现方式主要基于MYSQL,Redis,Zookeeper这三种,对比如下:

Redis实现分布式锁
Redis实现分布式锁的基本操作:
获取锁,利用setnx的互斥特性
setnx lock thread1
释放锁,手动释放
del lock
为防止死锁,加锁时设置超时时间
expire lock 10
setnx后添加属性,nx是互斥,ex是设置超时时长
setnx lock thread1 nx ex 10
获取锁时失败的话,要么阻塞失败等待释放锁,要么非阻塞失败立即返回结果。
非阻塞式锁效果如下,尝试一次成功返回true失败返回false

实现代码如下:
// 加锁的工具类
@Component
public class SimpleRedisLock implements ILock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String Name;
// 加锁方法
@Override
public boolean TryLock(long TimeoutSecond) {
long CurrentThreadId = Thread.currentThread().threadId();
Boolean SuccessOrFail = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+Name,CurrentThreadId+"",TimeoutSecond, TimeUnit.SECONDS);
return Boolean.TRUE.equals(SuccessOrFail);
}
// 去锁方法
@Override
public void UnLock() {
stringRedisTemplate.delete(KEY_PREFIX+Name);
}
}
修改服务层结构,将一人一单的判断以及下单记录封装成为独立方法,并且添加@Transactional保证余量更新和购买记录生效的一致性,随后对于该部分进行获取锁判断,由于加锁为非阻塞锁所以如果获取锁失败那么直接返回错误结果,如果获取锁成功那么执行业务,而无论业务是否成功都要释放锁
@Slf4j
@Service
public class VoucherOrderServiceImpl implements VoucherOrderService {
@Autowired
private VoucherSecondKillMapper voucherSecondKillMapper;
@Autowired
private VoucherOrderMapper voucherOrderMapper;
@Autowired
private RedisIdWorker redisIdWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
@Transactional
public ResultData VoucherSecondKill(Long VoucherId) {
VoucherSecondKillData Voucher = voucherSecondKillMapper.selectById(VoucherId);
if(Voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return ResultData.error("The SecondKill dose not start!");
}
if(Voucher.getEndTime().isBefore(LocalDateTime.now())) {
return ResultData.error("The SecondKill has ended!");
}
if(Voucher.getStock()<1) {
return ResultData.error("The Stock is not enough!");
}
long UserId = Long.parseLong(CurrentHolder.getCurrent().getId());
SimpleRedisLock lock = new SimpleRedisLock(ORDER_KEY_PREFIX+UserId,stringRedisTemplate);
boolean IsLock = lock.TryLock(1200);
if(!IsLock) {
return ResultData.error("Error:Repeat Order!");
}
try {
VoucherOrderService proxy = (VoucherOrderService) AopContext.currentProxy();
return proxy.CreateNewVoucherOrder(VoucherId);
} finally {
lock.UnLock();
}
}
@Override
@Transactional
public ResultData CreateNewVoucherOrder(Long VoucherId) {
Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());
QueryWrapper<VoucherOrderData> orderlwp = new QueryWrapper<VoucherOrderData>();
orderlwp.eq("voucher_id",VoucherId).eq("user_id",UserId).select("count(*) as Number");
List<Map<String,Object>> Result = voucherOrderMapper.selectMaps(orderlwp);
Long Count = (Long) Result.get(0).get("Number");
if(Count>0) {
return ResultData.error("You have already purchased this voucher!");
}
LambdaUpdateWrapper<VoucherSecondKillData> lwp = new LambdaUpdateWrapper<VoucherSecondKillData>();
lwp.eq(VoucherSecondKillData::getVoucherId,VoucherId);
voucherSecondKillMapper.UpdateSecondKillVoucher(lwp,1L);
VoucherOrderData NewVoucherOrder = new VoucherOrderData();
Long VoucherOrderId = redisIdWorker.NewId("Order");
NewVoucherOrder.setId(VoucherOrderId);
NewVoucherOrder.setUserId(UserId);
NewVoucherOrder.setVoucherId(VoucherId);
voucherOrderMapper.insert(NewVoucherOrder);
return ResultData.success(NewVoucherOrder);
}
}
Q:VoucherOrderService proxy = (VoucherOrderService) AopContext.currentProxy(); return proxy.CreateNewVoucherOrder(VoucherId);这两行代码的作用是什么,为什么不是执行调用return CreateNewVoucherOrder(VoucherId);
A:这段代码的核心目的是解决SpringBoot中“自调用”导致事务注解(@Transactional)失效的问题。
在Spring框架中,如果一个类内部的方法直接调用同一个类中的另一个带有@Transactional注解的方法,事务通常不会生效。因为正常调用Service层方法时请求的是Spring容器中的代理对象,而代理对象会先执行切面逻辑(如开启事务),然后再调用目标对象的真实方法。如果在 VoucherSecondKill方法内部使用this.CreateNewVoucherOrder(VoucherId)或直接调用CreateNewVoucherOrder(VoucherId),这是对象内部的直接方法调用。它绕过了代理对象,直接执行了目标方法。因此,CreateNewVoucherOrder方法上的@Transactional注解会被忽略,事务不会开启,如果发生异常也无法回滚。
为了解决这个问题,代码使用了AopContext.currentProxy()来获取当前对象的代理实例,这是SpringAOP提供的一个工具方法,其从当前线程的上下文(ThreadLocal)中获取绑定在当前线程上的代理对象实例,因为Spring在执行代理方法时,会将代理对象存入ThreadLocal,以便在方法执行过程中随时可以获取到当前代理版本,并通过代理对象来调用目标方法,从而确保事务逻辑被正确触发。
使用时还需要注意,由于代码显式获取当前AOP代理对象,所以需要开启代理暴露,配置方法为在配置类上添加@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class LeiShenCommentApplication {
public static void main(String[] args) {
SpringApplication.run(LeiShenCommentApplication.class, args);
}
}
@EnableAspectJAutoProxy的核心功能依赖于AspectJ的织入器。如果项目中没有引入aspectjweaver依赖,Spring 在初始化AOP上下文时会因为找不到相关类而崩溃。因此还需要添加依赖如下:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
否则会报错:
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2026-05-05T22:36:03.404+08:00 ERROR 5044 --- [LeiShenComment] [ main] o.s.boot.SpringApplication : Application run failed
封装常量
public class RedisConstants {
public static final String KEY_PREFIX = "lock:";
public static final String ORDER_KEY_PREFIX = "order:";
}
开始测试,首先要模拟集群下的多线程,在IDEA底边栏添加services处添加SpringBoot服务,随后添加新配置里选择添加虚拟机选项并设置端口

添加完成后,在获取锁处设置断点,重新启动服务,可以看到线程1获取锁成功,而线程二失败了


此时查看Redis数据库:

Redis锁误删问题
如果当前存在多线程并发,其中
线程1先启动并加锁但是由于某些原因导致线程阻塞,从而导致Redis锁超时被释放
线程2启动后获取锁成功并加锁,开始执行业务
线程1恢复后执行业务完成并释放锁(为防止死锁无论执行情况如何都要释放锁)
线程3启动后获取锁成功并加锁,开始执行业务,此时线程2和线程3并行执行
示意图如下,这显然不符合加锁的初衷

很显然,解决问题的关键在于释放锁时对锁进行判断

业务流程图修改如下:

解决锁误删问题
修改分布式锁实现,满足:
1.在获取锁时存入线程标示(可以用UUID表示)
2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致如果一致则释放锁
如果不一致则不释放锁
修改加锁工具类代码如下:
@Data
@AllArgsConstructor
public class SimpleRedisLock implements ILock{
private String Name;
private StringRedisTemplate stringRedisTemplate;
private static final String ID_PREFIX = UUID.randomUUID() +"-";
@Override
public boolean TryLock(long TimeoutSecond) {
// 针对Redis锁的value添加一个UUID,其中每一个线程的UUID都保证独一无二
String CurrentThreadId = ID_PREFIX + Thread.currentThread().getId();
Boolean SuccessOrFail = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+Name,CurrentThreadId,TimeoutSecond, TimeUnit.SECONDS);
return Boolean.TRUE.equals(SuccessOrFail);
}
@Override
public void UnLock() {
String CurrentThreadId = ID_PREFIX + Thread.currentThread().getId();
String LockThreadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX+Name);
if(CurrentThreadId.equals(LockThreadId)) {
stringRedisTemplate.delete(KEY_PREFIX+Name);
}
}
}
执行测试:
首先执行线程一,加锁完成


物理删除锁模拟线程一锁超时

随后执行线程二,加锁成功并且成功执行


重新执行线程一并释放锁,发现当前Redis分布式锁和线程一的锁不同,所以无法释放

继续执行线程二,发现当前Redis分布式锁和线程二的锁相同,所以可以释放

分布式锁原子性问题
如果线程一在判断进行锁判断完成后发生阻塞,阻塞完成后接下来继续执行去锁操作,依旧会导致已加的锁误删,示意图如下:

这时需要使用Lua脚本来解决问题@冰川日菜,一起噜♪起来!
Lua脚本
为了解决分布式锁在释放时可能出现的误删问题,需要保证“判断锁是否属于当前线程”和“删除锁”这两个操作的原子性。Lua脚本是解决这一问题的最佳方案,因为Redis执行Lua脚本时具有原子性,中间不会插入其他命令。
使用Lua语言执行Redis命令,其中agrs_num表示key类型参数。
EVAL "return redis.call(command,key,value)" args_num [arg...]

测试效果如下:


如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数(务必要严格区分大小写):


进一步改造分布式锁
首先编写Lua脚本unlock.lua,其中KEYS代表锁在Redis中的Key,ARGV代表当前尝试解锁的线程的唯一标识:
-- 比较线程中标识和锁的标识是否一致
if(redis.call('get',KEYS[1]) == ARGV[1]) then
-- 如果一致,说明锁确实是当前线程持有的,则去锁
return redis.call('del',KEYS[1])
end
-- 如果不一致(说明锁已过期或被他人持有),则不执行删除,直接返回0
return 0
修改分布式锁工具类如下:
@Data
@AllArgsConstructor
public class SimpleRedisLock implements ILock{
private String Name;
private StringRedisTemplate stringRedisTemplate;
private static final String ID_PREFIX = UUID.randomUUID() +"-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); // 设置脚本文件位置
UNLOCK_SCRIPT.setResultType(Long.class); // 设置返回结果类型
}
@Override
public boolean TryLock(long TimeoutSecond) {
String CurrentThreadId = ID_PREFIX + Thread.currentThread().getId();
Boolean SuccessOrFail = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+Name,CurrentThreadId,TimeoutSecond, TimeUnit.SECONDS);
return Boolean.TRUE.equals(SuccessOrFail);
}
@Override
public void UnLock() {
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX+Name), // KEYS
ID_PREFIX + Thread.currentThread().getId() // ARGV
);
}
}

浙公网安备 33010602011771号