Redis 实战篇1.3(分布式锁)

张开发
2026/4/13 12:57:56 15 分钟阅读

分享文章

Redis 实战篇1.3(分布式锁)
一人一单的并发安全问题理想情况单个JVM在集群模式下多个JVM有多个锁监视器无法使互斥锁互斥,导致并发问题解决分布式锁使多个JVM使用同一个锁监视器就可以使不同JVM中的并发线程共用一个锁监视器互斥锁避免并发问题什么是分布式锁分布式锁的实现基于Redis的分布式锁直接可以开始代码案例.核心方法setIfAbsent(...)这是整个加锁逻辑的灵魂对应 Redis 的SET key value NX EX timeout命令原子性实现了 3 个关键能力简单案例代码public interface ILock { /** * 尝试获取锁 * param timeoutSec 锁的超时时间单位秒 * return 成功返回true失败返回false */ boolean tryLock(long timeoutSec); /** * 释放锁 */ void unlock(); }public class SimpleRedisLock implements ILock{ private StringRedisTemplate stringRedisTemplate; // 锁的名称 private String name; // 锁的key前缀 private static final String KEY_PREFIX lock:; public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name name; this.stringRedisTemplate stringRedisTemplate; } Override public boolean tryLock(long timeoutSec) { // 获取线程标识 Long threadId Thread.currentThread().getId(); // 获取锁 Boolean success stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX name, threadId 1, timeoutSec, TimeUnit.SECONDS); // 这里如果success为null,不进行判断就会发生空指针异常 // 所以添加判断 return Boolean.TRUE.equals( success); } Override public void unlock() { // 释放锁 stringRedisTemplate.delete(KEY_PREFIX name); } }/** * 秒杀优惠券 * param voucherId * return */ Override public Result seckillVoucher(Long voucherId) { // 1.查询优惠券 SeckillVoucher voucher seckillVoucherService.getById(voucherId); // 2.判断秒杀是否开始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { // 尚未开始 return Result.fail(秒杀尚未开始); } // 3.判断秒杀是否已经结束 if (voucher.getEndTime().isBefore(LocalDateTime.now())) { // 尚未开始 return Result.fail(秒杀已经结束); } // 4.判断库存是否充足 if (voucher.getStock() 1) { // 库存不足 return Result.fail(库存不足); } // 从这里开始下面多个服务器中并发的线程用的锁就是同一个锁 Long userId UserHolder.getUser().getId(); // 创建锁对象 SimpleRedisLock simpleRedisLock new SimpleRedisLock(order: userId, stringRedisTemplate); boolean isLock simpleRedisLock.tryLock(888); if (!isLock) { // 获取锁失败返回失败或者重试 return Result.fail(统一用户不允许重复下单); } try { // 获取代理对象确保事务生效因为不获取代理对象调用方法则那个方法就是this类调用不会生效事务 IVoucherOrderService proxy (IVoucherOrderService) AopContext.currentProxy(); // 8.返回订单id return proxy.createVoucherOrder(voucherId); } catch (Exception e) { throw new RuntimeException(e); } finally { // 7.释放锁 simpleRedisLock.unlock(); }紧接着又出现问题了这个是优化后的通过UUID工具类创建一个锁就是存入redis唯一的value在释放锁方法释放锁之前先判断存入Redis中的锁的唯一标识是否是当前线程的如果是再释放锁如果不是不要释放锁直接跳过// 线程标识 private static final String ID_PREFIX UUID.randomUUID().toString(true)-; Override public boolean tryLock(long timeoutSec) { // 获取线程标识 每一个线程获取的锁都是唯一的 String threadId ID_PREFIX Thread.currentThread().getId(); // 获取锁 Boolean success stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX name, threadId 1, timeoutSec, TimeUnit.SECONDS); // 这里如果success为null,不进行判断就会发生空指针异常 // 所以添加判断 return Boolean.TRUE.equals( success); } Override public void unlock() { // 获取线程标识 String threadId ID_PREFIX Thread.currentThread().getId(); // 获取存入Redis中锁中的标识 String id stringRedisTemplate.opsForValue().get(KEY_PREFIX name); if(threadId.equals(id)){ // 释放锁 stringRedisTemplate.delete(KEY_PREFIX name); } }继而又出现了问题就在这个之间又出现了特别极端的问题已经判断了这个线程的标识和redis中的标识一样后发生了阻塞继而Redis超时释放了锁后其他线程有获取到了这个互斥锁此时当前线程阻塞状态消失因为已经做出了判断把锁释放掉了但是释放的是其他线程的互斥锁这样又发生了多线程并发问题解决Redis的Lua脚本https://www.runoob.com/lua/lua-tutorial.html写出Lua脚本使判断锁线程标识与释放锁的原子性写在Java代码中,RedisTemplate调用Lua脚本的api明天继续编写

更多文章