Redis的分布式锁问题(八)基于Redis的分布式锁

Redis的分布式锁问题(八)基于Redis的分布式锁

分布式锁 

什么是分布式锁? 

成为分布式锁的必要条件

分布式锁的实现方案

基于Redis实现分布式锁

关于锁的两个级别操作

获取锁

释放锁

如果在添加过期时间后宕机了呢? 

Redis分布式锁实现秒杀下单(初级版本)

代码实现 

测试结果 

存在问题

解决方案


Redis的分布式锁问题(八)基于Redis的分布式锁

分布式锁 

什么是分布式锁? 

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 

当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

传统单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。

但是在分布式系统,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁的由来。

成为分布式锁的必要条件

分布式锁的实现方案

分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,市面上常见大致有三种分布式锁的实现方案

关于MySQL锁机制可以看看这里

【图灵MySQL】深入理解MySQL事务隔离级别与锁机制_面向鸿蒙编程的博客-CSDN博客https://blog.csdn.net/weixin_43715214/article/details/127594907

基于Redis实现分布式锁

关于锁的两个级别操作

关于锁的两个级别操作就是——获取锁、释放锁

获取锁

互斥,要确保只能有一个线程后去锁!

添加锁,利用setnx的互斥特性 

setnx lock thread

添加锁的过期时间,避免服务宕机引起的死锁! 

expire lock 10

释放锁

手动释放 

del lock

超时释放 

expire命令添加了过期时间,等时间到了自动释放!

如果在添加过期时间后宕机了呢? 

在redis中,获取锁添加锁的命令可以一起执行,一条redis的命令中,是具有原子性的! 

最优的写法如下 

set lock thread1 nx ex 10

Redis分布式锁实现秒杀下单(初级版本)

代码实现 

编写ILock接口,定义锁  

/**
 * redis的分布式锁
 */
public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功; false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

实现ILock接口,编写tryLock()和unLock()逻辑 

/**
 * redis的分布式锁
 * 实现ILock接口
 */
public class SimpleRedisLock implements ILock {

    // 不同的业务有不同的锁名称
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        // set lock thread1 nx ex 10
        // nx : setIfAbsent , ex : timeoutSec
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 自动拆箱!!!可能有风险
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

业务逻辑 

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result seckillVoucher(Long voucherId) {

        // 1. 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        LocalDateTime nowTime = LocalDateTime.now();

        // 2. 判断秒杀是否开始
        if (nowTime.isBefore(voucher.getBeginTime())) {
            return Result.fail("活动未开始!");
        }

        // 3. 判断秒杀是否结束
        if (nowTime.isAfter(voucher.getEndTime())) {
            return Result.fail("活动已结束!");
        }

        // 4. 判断库存
        if (voucher.getStock() < 1) {
            return Result.fail("已买完!");
        }

        Long userId = UserHolder.getUser().getId();

        // 创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 获取锁 1200s
        boolean isLock = lock.tryLock(1200);

        if (!isLock) {
            return Result.fail("不允许重复下单!");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 一人一单
        Long userId = UserHolder.getUser().getId();

        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0){
            return Result.fail("用户已经购买过一次了!");
        }

        // 5. 减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock", 0)  // CAS方案(乐观锁)!
                .update();

        if (!success) {
            return Result.fail("库存不足");
        }

        // 6. 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();

        // 6.1 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2 用户id
        voucherOrder.setUserId(userId);
        // 6.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return Result.ok(orderId);
    }
}

从之前的 synchronized 到现在的 isLock

// 创建锁对象

SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);

// 获取锁 1200s

boolean isLock = lock.tryLock(1200);

if (!isLock) {

        return Result.fail("不允许重复下单!");

}

try {

        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();         return proxy.createVoucherOrder(voucherId);

} finally {

        lock.unlock();

测试结果 

8081端口 

isLock的结果是false,所以不会被锁住,执行后续的业务逻辑 

8082端口 

isLock的结果是true,所以会被锁住,直接return

存在问题

这么看起来视乎是没有问题了,但是我们来看看下面的这种特殊情况!

由于线程1的业务阻塞,线程1的锁超时释放了,所以此时线程2就可以获取线程1刚刚释放的锁

当线程2开始执行自己的业务代码时,线程1的业务正巧又执行完了,所以刚刚线程2获取的锁会被线程1释放!

此时又来了一个线程3,由于此时的状态是“无锁”,所以线程3也可以获取锁,那么现在就变成了线程2、3同时在执行业务代码!!!

这样子就可能发生线程安全的问题

这就是Redis分布式锁误删问题!!!

解决方案 

发生这个问题的主要原因。主要是在各个线程之间释放锁的机制有问题!我们要让线程只能释放自己上的锁,不能释放其他线程的锁!(自己的事情自己做

修改一下释放锁的逻辑,每一个线程只能删除自己的锁!!! 

redis中数据样例

代码如下

我们可以用当前线程的id与redis的值相比较,来确定是不是同一个! 

@Override
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

posted @   金鳞踏雨  阅读(60)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示