Redis实现分布式锁

Redis数据结构:String

超卖and一人一单问题

相关前言问题:
超卖解决:
       方案(乐观锁):只需要在扣减库存更新数据库时带上条件 库存>0(CAS法)
boolean success = iSeckillVoucherService.update()
                        .setSql("stock = stock - 1")
                        .eq("voucher_id", voucherOrder.getVoucherId())
                        .gt("stock", 0)
                        .update();
       方案(悲观锁):添加同步锁让线程串行执行。(简单粗暴,性能下降)
​
           
一人一单:
      单体项目:
          一:如果是更新数据:则可以在购买更新前先做判断
       int count = query().eq("user_id", userId).eq("voucher_id",voucherId).count();
       // 5.2.判断是否存在
       if (count > 0) {
        // 用户已经购买过了
        return Result.fail("用户已经购买过一次!");
    }
          二:加锁
    public synchronized Result CreateVoucherOrder(Long voucherId) {}
​
      集群模式:
            添加锁监视器(Mysql本身互斥锁实现、Redis中setnx的互斥命令实现、Zookeeper利用节点唯一性和有序性实现)

 

基于setnx实现分布式锁

存在问题:不可重入 (同一个线程在已经持有锁的情况,再次尝试获取同一把锁时会失败; 假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,就死锁了)

不可重试 (线程尝试获取锁失败后,不会再次尝试获取,而是直接返回失败结果)

超时释放 (通常需要手动设置锁的过期时间,以避免锁被永久占用;二是如果锁的过期时间设置不合理,可能会导致业务逻辑还未执行完,锁就已经过期释放,从而引发并发问题)

主从一致性:(如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。)

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
   // 获取线程标示(直接使用Thread.currentThread() 只在当前线程的JVM上是唯一的,而如果涉及多个JVM则会导致冲突,存在可能误删问题)
   String threadId = ID_PREFIX + Thread.currentThread().getId();
   // 获取锁
   Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}
​
//释放锁版本一:
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    // !!!这里如果线程1在判断完后,锁正好到期了,那么此时线程2进来获取锁,然而线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,把线程2的锁给释放了
    // 因此要保证分布式锁的原子性->Lua
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}
​
​
//释放锁版本二:
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
​
public void unlock() {
    // 调用lua脚本
    stringRedisTemplate.execute(
            //上面定义好的 Lua 脚本对象。
            UNLOCK_SCRIPT, 
            // KEYS 参数传入脚本,也就是锁的键
            Collections.singletonList(KEY_PREFIX + name), 
            // ARGV 参数传入脚本,即当前线程的标识
            ID_PREFIX + Thread.currentThread().getId()); 
}
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

 

Redission实现分布式锁

可重入:用state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如来一个人持有这把锁,那么state++,释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有。

分布式锁-redission锁重试和WatchDog机制:先判断当前这把锁是否存在,如果不存在,插入一把锁,返回null;判断当前这把锁是否是属于当前线程,如果是,则返回null;所以如果返回是null,则代表着当前这哥们已经抢锁完毕。如果是没有传入时间,则此时也会进行抢锁, 而且抢锁时间是默认看门狗时间 。

redission锁的MutiLock原理:使用这把锁不使用主从,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,保证了加锁的可靠性。

// 引用
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
​
    
// 配置类    
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setPassword("123456");
        return Redisson.create(config);
    }
}
​
​
@Resource
private RedissonClient redissonClient;
​
@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 lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
       
        //加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
           //获取代理对象(事务)来调用方法
           // 拿到IVoucherOrderService接口代理对象进行操作, 防止事务失效
           // (Transactional事务是用Spring的代理对象来完成的, 而这里直接调用createVoucherOrder则是使用this.的方式调用,会导致事务失效)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
 }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值