基于Redis的分布式锁实现(秒杀优惠券的优化)

目录

什么是分布式锁​

基于Redis的分布式锁(非阻塞实现)

当业务阻塞时出现的并发问题(误删Redis分布式锁)

总结


前文链接:

(1条消息) Redis——实现优惠券秒杀_486过于烦躁的博客-CSDN博客

什么是分布式锁

修改秒杀优惠券的流程

分布式锁的实现


基于Redis的分布式锁(非阻塞实现)

 

 ILock接口

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

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

创建一个类并实现ILock接口,用自定义的name值来区分锁,同时获取当前线程的Id作为value值存入进redis中,并设置超时时间

public class SimpleRedisLock implements ILock {

    private static final String KEY_PREFIX="lock:";

    private String name;

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean trLock(long timeoutSec) {
        //获取线程标识
        long threadId = Thread.currentThread().getId();
        //获取锁
        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);
    }
}

实现

   @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //获取用户Id
        UserDTO user = UserHolder.getUser();
        Long userId = user.getId();

        //创建锁对象
        SimpleRedisLock redisLock = new SimpleRedisLock("order" + userId, stringRedisTemplate);

        //尝试获取锁
        boolean isLock = redisLock.trLock(5);
        //判断
        if(!isLock){
            //获取锁失败,直接返回失败或者重试
            return Result.fail("不允许重复下单");
        }

        try {
            //一人一单
            Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            if(count>0){
                //说明用户之前买过了
                return Result.fail("用户已经购买过一次");
            }

            VoucherOrder voucherOrder = new VoucherOrder();

            //扣减库存
            seckillVoucherService.update()
                    .setSql("stock = stock - 1")
                    .eq("voucher_id",voucherId).gt("stock",0)
                    .update();

            //设置订单Id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            //设置优惠券Id
            voucherOrder.setVoucherId(voucherId);
            //设置用户Id
            voucherOrder.setUserId(userId);

            save(voucherOrder);

            //返回订单Id
            return Result.ok(orderId);
        } finally {
            //释放锁
            redisLock.unlock();
        }
    }

当业务阻塞时出现的并发问题(误删Redis分布式锁)

可能会出现误删Redis分布式锁的情况

修改流程 ,需要判断锁标示是否是自己的

 

需要确保标示是唯一的,可以采用UUID拼接线程Id的做法

业务代码不需要改,只需要修改锁的获取与释放,增加逻辑判断

public class SimpleRedisLock implements ILock {

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";

    private String name;

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean trLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @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);
        }
    }
}


虽然增加了锁标示的判断,但仍然存在一些极端的问题,如下图,当判断标示与释放锁之间产生了阻塞,也有可能发生误删分布式锁的情况,所以得让判断标示与释放锁成原子性操作,要么一起成功,要么一起失败

我们可以采用Redis中的Lua脚本进行编写,然后在Java程序中进行调用

Redis的Lua脚本

Lua语言的数组第一个下标为1

业务逻辑转换为Lua脚本

再次改进Redis的分布式锁

unlock.lua脚本的编写 

-- 比较线程标识是否是锁的一致
if(redis.call('get',KEYS[1]==ARGV[1])) then
    -- 释放锁
    return redis.call('del',KEYS[1])
end
return 0

实现

public class SimpleRedisLock implements ILock {

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    private String name;

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean trLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    //基于Lua脚本
    @Override
    public void unlock() {
        //调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

}

总结

仍存在优化的空间

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值