分布式锁,可重入锁机制


/**
 * <br>
 * <br>
 *
 * 
 * @date 2022-2-18 14:53
 */
@RestController
public class RedissonController {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Resource
    Redisson redisson;

    /**
     * 一段代码看出redis做锁的问题,为什么要用redisson
     * stringRedisTemplate.opsForValue().setIfAbsent()
     * 这段代码的底层用的是setnex的命令,此命令的特性就是,如果在相同key的情况下,你设置了第一个key成功返回true之后,第二次再设置就会返回false不成功
     * 问题:高并发的情况下
     * 1.
     * Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId);
     * 在这段代码运行的时候,还没有运行到stringRedisTemplate.delete(lockKey)删除锁的时候,中途服务宕机了就会造成死锁
     *
     *2.当出现死锁的情况下加一段代码
     *     stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
     * 设置redis的key的失效时间,引发的问题:
     * 高并发情况下,第一条线程还没走到设置超时时间的时候,突然服务就挂了,还是会出现死锁
     * 所以设置锁和失效时间必须要有原子性(合并代码)解决:
     * Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
     *  这样还是会引发问题:
     *  高并发情况下,如果第一条线程进来了,还没走到删除哪里就已经超过10秒了,停顿了等待恢复,那么第二条线程进来了,就占用了锁,然后第一条线程突然恢复过来,执行了删除锁的逻辑
     *  那么这个时候删除的就是第二条线程的锁了,就会造成永久死锁
     *  解决方案:再finally模块做一个判断逻辑删除:
     *            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
     *                 stringRedisTemplate.delete(lockKey);
     *             }
     * 此时还会引发出一个问题:
     * 在这段代码clientId.equals(stringRedisTemplate.opsForValue().get(lockKey)执行后,突然服务出现卡顿,超过了10秒,那么锁又失效了,第二条线程进来又抢到了锁,突然第一条
     * 线程又开始执行了stringRedisTemplate.delete(lockKey); 这个时候删除的就是第二条线程的锁,还是会出现上面的问题;
     * 解决方案: 超时时间做续期
     * 当第一条线程进来的时候,开分线程,开一个定时器,分线程每过一段时间查询一下这条主线程是否还拥有这把锁,如果还拥有,那么就给这把锁的失效时间做一个续期
     *
     * 因为实现麻烦,所以引入框架,redisson
     */
    @RequestMapping("/redisLock")
    public String redissonLock(){

        String lockKey="xxx";//一般用订单id+前缀或者后缀做锁的key
        //做value
        String clientId = UUID.randomUUID().toString();

        //Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId);
        //stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);

        if (!result){
            return "errorCode";
        }
        try{
            //获取库存
            int stock= Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock>0){
                int realStock=stock-1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                System.out.println("扣减成功,剩余库存:"+realStock);
            }else {
                System.out.println("扣减失败,库存不足");
            }
        }finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }


    /**
     * 引入redisson框架
     *
     * 底层实现,lua脚本执行,执行后会有一个分线程,用的是定时调度线程池,底层也是执行lua脚本,首先判断主线程的key是否占用了锁,如果占用到锁,那么就重新给锁的过期时间续费,如果没有就不执行
     *
     * 在此框架中,实际就是将并发线程串行起来的,又会导致性能问题产生
     * 解决方案:分段锁
     * 比如 库存有200个,我分成10个段,那么再分配到redis的集群中去,通过负载均衡算法去拿,就提高了性能
     *
     *
     *
     *
     * @return
     */
    @RequestMapping("/redisLockssss")
    public String redisisonLock(){

        String lockKey="xxx";//一般用订单id+前缀或者后缀做锁的key

        RLock redissonLock = redisson.getLock(lockKey);
        try{
            redissonLock.lock();
            //获取库存
            int stock= Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock>0){
                int realStock=stock-1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                System.out.println("扣减成功,剩余库存:"+realStock);
            }else {
                System.out.println("扣减失败,库存不足");
            }
        }finally {
            redissonLock.unlock();
        }
        return "end";
    }

    /**
     *
     * redis的过期键的删除策略:
     * 惰性过期:有点像懒加载的模式(按需加载),只有当你访问一个key的时候,才会去判断该Key是否过期,过期则清除,最大化的节省了cpu资源,极端情况下可能大量的key没有被再次访问,占用大量内存
     *
     * 定期过期:每隔一定的时间,会去扫描一定数量的数据库的expires字典中一定数据的key,并清除已过期的key
     *
     */

    /**
     * redis的线程模型,为什么单线程快:
     *
     * 1.纯内存操作
     * 2.核心是基于非阻塞的IO多路复用机制
     * 3.单线程反而避免了多线程的繁复上下文切换带来的性能问题
     *
     * =================================================
     *
     *项目中应用锁得机制:
     * 可重入锁:表示第一条线程和第二条线程并发进入得啥时候,抢到锁得那个线程如第一条线程,哪怕他执行完了后,还是可以重新进入,可重试
     * 首先tryLock内部封装的是setnex  如果没有,那么就set进去一条值
     *
     *
     * 参数1:自定义key值 以下方法是自定义的key,表示锁整个方法
     * 参数2: redission tryLock 尝试获取锁的时间 :表示第二条线程进入尝试获取锁时间,若时间过去了,则抛出线程终端异常
     * 参数3:redission tryLock 锁持有时间 :表示线程获取到锁,执行业务时长,如果超过了,那么就会自动释放锁,第二条线程即可进入
     * 参数4:单位时间
     *
     * lockHelper.tryLock(AppConstant.KIRIN_SALES_CARPACKAGE_REDIS_KEY, AppConstant.WAIT_TIME, AppConstant.LEASE_TIME, TimeUnit.SECONDS)
     *
     */

/*    @Transactional(rollbackFor = Exception.class)
    public Boolean saveCarPackage(Object obj) {
        try {
            if (lockHelper.tryLock(AppConstant.KIRIN_SALES_CARPACKAGE_REDIS_KEY, AppConstant.WAIT_TIME, AppConstant.LEASE_TIME, TimeUnit.SECONDS)) {
                //业务逻辑
        } catch (InterruptedException e) {
            log.error("获取锁失败:", e);
            Thread.currentThread().interrupt();
            throw new BusinessException("服务异常,请重新再试");
        } catch (BusinessException ex) {
            log.error("业务异常:", ex);
            throw new BusinessException(ex.getCode(), ex.getMessage());
        } finally {
            if (lockHelper.isLocked(AppConstant.KIRIN_SALES_CARPACKAGE_REDIS_KEY)) {
                lockHelper.unlock(AppConstant.KIRIN_SALES_CARPACKAGE_REDIS_KEY);
            }
        }
        return false;
    }*/


    /**
     * 项目中第二种锁机制
     * 可重入锁:表示线程获取到锁之后,哪怕执行完后,也可以重新进入锁机制:可重试
     * tryLock()内部封装的是setnex表示如果没有这个key那么就set进去一个,如果有就不能set了
     *
     * 方法参数:
     * 参数1:这里使用的是前缀加商品id:作用是区分每一个商品,如:两天线程拿着相同的商品id进入,第一条线程进入,第二条线程没有获取到锁等待尝试获取锁,当第一条线程跑完了,第二条进入,内部业务逻辑卡此商品id
     *参数2:redission tryLock 尝试获取锁的时间 :表示第二条线程等待尝试获取锁的时长,若是时间过期,没有获取到,则抛出中断异常
     * 参数3:redission tryLock 锁的持有时间:表示锁的执行业务时长,若是此线程时间到了还没有执行完成,那么就自动释放锁,让第二条线程进入
     * 参数4:单位时间
     *
     *
     * lockHelper.isLocked(lockId) :表示判断是否是属于这个key,防止删除其他线程
     * lockHelper.unlock(lockId):释放此线程
     *
     */

/*    @Transactional(rollbackFor = Exception.class)
    public Boolean executeOrderCallback(Object obj) {

        String lockId = RedisPrefix.REDIS_LOCK_CREATE_DOWN_PREFIX + salesOrderCallbackVO.getSalesInfoId();
        try {
            if (lockHelper.tryLock(lockId, WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS)) {
                //执行业务方法
            }
        } catch (InterruptedException e) {
            log.error("获取锁失败:" + salesOrderCallbackVO.getSalesInfoId(), e);
            Thread.currentThread().interrupt();
            throw new BusinessException("该单已被其他订单下定,无法再收定金");
        } finally {
            if (lockHelper.isLocked(lockId)) {
                lockHelper.unlock(lockId);
            }
        }
        return false;
    }*/

    /**
     *  Thread.currentThread().interrupt(); 表示做个可以中断线程的标记了
     *  什么是中断:
     *  在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断。
     *
     *  中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupted方法,该方法也仅仅是将线程对象的中断标识设成true;
     *  接着你需要自己写代码不断地检测当前线程的标识位;如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
     *  每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
     *  通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
     *
     *  如何使用中断:
     *  要使用中断,首先需要在可能会发生中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理代码。
     *  当需要中断线程时,调用该线程对象的interrupt函数即可。
     */

/*
    Thread thread = new Thread(() -> {
        while (!Thread.interrupted()) {
            // do more work.
        }
    });
thread.start();

// 一段时间以后
thread.interrupt();
*/



}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值