• 解决集群环境下的并发问题【分布式锁】


    在集群环境下锁失效的原因:

    由于现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

    常见分布式锁对比

    在这里插入图片描述

    Redis分布式锁

    实现分布式锁时需要实现的两个基本方法:

    • 获取锁:
      • 互斥:确保只能有一个线程获取锁
      • 非阻塞:尝试一次,成功返回true,失败返回false
    • 释放锁:
      • 手动释放
      • 超时释放:获取锁时添加一个超时时间

    核心思路:

    我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可
    在这里插入图片描述

    获取锁的代码:

    private static final String KEY_PREFIX="lock:"
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = Thread.currentThread().getId()
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    释放锁的代码:

    public void unlock() {
        //通过del删除锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
    
    • 1
    • 2
    • 3
    • 4

    修改业务逻辑代码:

     @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);
            //获取锁对象
            boolean isLock = lock.tryLock(1200);
    		//加锁失败
            if (!isLock) {
                return Result.fail("不允许重复下单");
            }
            try {
                //获取代理对象(事务)
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    分布式锁误删问题

    逻辑说明:

    持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

    解决方案:解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。
    在这里插入图片描述

    解决方案:

    需求:修改之前的分布式锁实现,满足:在获取锁时存入线程标示(可以用UUID表示)

    在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

    • 如果一致则释放锁
    • 如果不一致则不释放锁

    核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。

    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, 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);
        // 判断标示是否一致
        if(threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    分布式锁的原子性问题

    线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁,但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生,
    在这里插入图片描述

    解决方案:【Lua脚本】

    Lua脚本
    -- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
    -- 获取锁中的标示,判断是否与当前线程标示一致
    if (redis.call('GET', KEYS[1]) == ARGV[1]) then
      -- 一致,则删除锁
      return redis.call('DEL', KEYS[1])
    end
    -- 不一致,则直接返回
    return 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    java调用Lua脚本

    我们的RedisTemplate中,可以利用execute方法去执行lua脚本,参数对应关系就如下图股
    在这里插入图片描述

    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(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    分布式锁Redission

    Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

    redission 的基本配置

    引入依赖:

    <dependency>
    	<groupId>org.redisson</groupId>
    	<artifactId>redisson</artifactId>
    	<version>3.13.6</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    配置Redssion客户端:

    @Configuration
    public class RedissonConfig {
    
        @Bean
        public RedissonClient redissonClient(){
            // 配置
            Config config = new Config();
            config.useSingleServer().setAddress("redis://192.168.150.101:6379")
                .setPassword("123321");
            // 创建RedissonClient对象
            return Redisson.create(config);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用:

    @Resource
    private RedissionClient redissonClient;
    
    @Test
    void testRedisson() throws Exception{
        //获取锁(可重入),指定锁的名称
        RLock lock = redissonClient.getLock("anyLock");
        //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
        boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
        //判断获取锁成功
        if(isLock){
            try{
                System.out.println("执行业务");          
            }finally{
                //释放锁
                lock.unlock();
            }
            
        }
        
    }
    
    
    @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 proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
            } finally {
                //释放锁
                lock.unlock();
            }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
  • 相关阅读:
    Java之分支结构
    WEB网页设计期末作业个人主页——基于HTML+CSS制作个人简介网站
    2023NOIP A层联测25-游戏
    es6运算符扩展
    Java 如何经行异常处理?常见的运行异常的编译时异常
    JJJ:PCI / PCIE 的一些术语和概念
    SpringMVC框架之Controller方法的返回值
    Vue+Element-ui+SpringBoot搭建后端汽车租赁管理系统
    【数据结构】List、Set、Map的联系和区别(通俗易懂,清晰直观!!)
    一、python Django基础配置[基础内容]
  • 原文地址:https://blog.csdn.net/weixin_52162723/article/details/127541597