• 生产级Redis 高并发分布式锁实战1:高并发分布式锁如何实现


    高并发场景:秒杀商品。

    秒杀一般出现在商城的促销活动中,指定了一定数量(比如:1000个)的商品(比如:手机),以极低的价格(比如:0.1元),让大量用户参与活动,但只有极少数用户能够购买成功.

    示例代码

    @RestController
    public class IndexController {
        
        @autowired
        private Redisson redisson;
        @Autowired
        private StringRedisTemplate stringRedisTemplate
        
        @RequestMapper("/stock/deduct_stock")
        public String deductStock(){
            
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));  // jedis.get("stock")
            
            if(stock > 0){
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock",realStock + "");  // jedis.set(key,value)
                
                System.out.print("======商品扣减成功,剩余库存:"+ realStock);
                
            } else {
                System.out.print("======商品扣减失败======");
            }
            
            return "---=== end ===---";
        }
        
        
        
    }
    
    

    问题

    假设10个线程同时扣减1000个商品,应该剩余990 个商品,但实际扣减剩余999个,发生超卖问题。

    • 解决方案

    加锁。synchronized(this)

    jdk 内置锁在分布式环境下并不能保证成功。

    模拟高并发场景,部署测试环境。

    可用jMeter 工具进行压力测试。

    添加ThreadGroup,并在栏目下添加 Http Request。配置并发计划。
    可选择添加Aggregate Report 报告。

    完整代码:

    
     @RequestMapper("/stock/deduct_stock")
        public String deductStock(){
            
            synchronized(this){
                int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            
                if(stock > 0){
                    int realStock = stock - 1;
                    stringRedisTemplate.opsForValue().set("stock",realStock + "");
                    
                    System.out.print("======商品扣减成功,剩余库存:"+ realStock);
                    
                } else {
                    System.out.print("======商品扣减失败======");
                }
                
            }
            return "---=== end ===---";
        }
    

    启动多台服务器,测试日志结果可知,发生超卖问题。

    添加分布式锁

    redis 命令setnx key value

    setnx 是“set if not exists” 的缩写。

    含义是:

    • 将key 的值设为value, 当且仅当key 不存在。
    • 若给定的key 已经存在,则setnx 不做任何操作。

    注:加锁完成后,要删除锁。

    完整代码:

        String lockKey = "lock:product_100";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock,"amumu")
    
        if (!result){
            return "---=== error_code ---===";
        }
        
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    
        if(stock > 0){
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock",realStock + "");
            
            System.out.print("======商品扣减成功,剩余库存:"+ realStock);
            
        } else {
            System.out.print("======商品扣减失败======");
        }
        
        stringRedisTemplate.delete(lockkey);
        return "---=== end ===---";
    
    

    分布式锁下的原子性问题优化

    发现问题
    • 在set过程中程序异常,则程序无法继续执行,造成死锁。
    //获取异常并抛异常
    try{
        
    } finally{
        
    }
    
    • 在set 过程中,系统宕机,或系统重启。
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,"ammumu");
    
    
    //添加过期时间
    stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);
    
    

    上述代码执行过程中,程序异常情况下也会导致系统宕机,发生原子性问题,

    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,"amumu",10, TimeUnit.SECONDS);
    
    

    分布式锁的失效情况优化

    • 假设 10s 的过期时间。

    假设多个请求,每个程序执行超过10s, 此时锁已过期。此时程序执行删除锁,导致此时锁失效。

    分析问题:加锁过程,被其他线程执行删锁。

    解决方案:加上锁标识,删除时判断是否是同一标识。

    String clientId = UUID.randomUUID().toString();
    // 加上cliendId 锁标识
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,clientId, 10, TimeUnits.SECONDS);
    
    
    
    //删除锁时,进行判断
    if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
        stringRedisTemplate.delete(lockkey);
    }
    
    
    • 假设执行删除锁前,锁快过期未过期。下一个并发线程执行加锁操作,此时上一个并发线程,执行了删除操作。锁依然会失效。

    解决方案:锁续命。如redisson

    参考:github.com/redisson/redisson

    每隔10s 检查是否还持有锁,如果持有,则延长30s锁的时间

    参考:redis lua 脚本

    RLock redissonLock = redisson.getLock(lock); // 获取锁对象
    redissonLock.lock(); // 加锁 相当于 setIfAbsent(lockkey,"amumu",10, TimeUnit.SECONDS);
    
    
    redissonLock.unlock();
    
    

    完整代码:

        @autowired
        private Redisson redisson;
        @Autowired
        private StringRedisTemplate stringRedisTemplate
        
        @RequestMapper("/stock/deduct_stock")
        public String deductStock(){
            
            String lockKey = "lock:product_101";
            
            RLock redissonLock = redisson.getLock(lockKey);
            redissonLock.lock(); // 加锁
            
            try{
                int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            
                if(stock > 0){
                    int realStock = stock - 1;
                    stringRedisTemplate.opsForValue().set("stock",realStock + "");
                    
                    System.out.print("======商品扣减成功,剩余库存:"+ realStock);
                    
                } else {
                    System.out.print("======商品扣减失败======");
                }
            }finally{
                redissonLock.unlock();
            }
            
            
            return "---=== end ===---";
        }
    
    
    锁续命业务优化
    • 问题1 :线程1 加锁,此时未同步给从结点,主结点挂了。此时从结点很可能会被选为主结点。这时一个线程3,同时请求此操作,此时线程3 操作新选为主结点redis 加锁,此时程序继续执行,执行有并发安全问题的代码。

    • 问题2 :redis 语义上,是把高并发场景下的问题串行化了,这当然不会有并发问题,但是对整个系统的性能有影响。当然redis 的性能已经是非常不错了。

    • 问题3:redissonLock.unLock() 底层默认的是非公平的抢锁机制,while 循环尝试去加锁。

    redis 与 zookeeper 分布式集群方案对比

    根据CAP 理论:
    redis 集群更多是保证AP, 即可用性。

    zookeeper 集群更多是保证CP, 即一致性,ZAB写数据机制保证,不会有锁丢失的问题。

    redlock 实解决redis 集群环境锁丢失问题

    想用redis 的高性能,但是又不想丢失锁,有很多方法。

    比如 Redlock 实现。

    java client 加锁三个以上的锁,超过半数redis 节点加锁成功才算加锁成功。

    • 问题:千万不要用从节点。要保证集群高可用,多加几个节点。
    • 问题:持久化。一般用aof 做持久化,假设设置1s 持久化一次。
      这样就设置一个key,设置2个key 时系统宕机或重启。client2 执行请求,key3 加锁成功,key2 重启后加锁成功,则加锁成功。系统继续执行的是并发安全问题的代码。如果用时时持久化设置,则性能会大大降低,还不如用zookeeper.

    高并发分布式锁如何实现

    分段加锁逻辑。

    未完待续
    by: 一只阿木木

  • 相关阅读:
    POJ C程序设计进阶 编程题#4:Tomorrow never knows?
    POJ C程序设计进阶 编程题#3: 发票统计
    深度学习笔记(10)- 控制中心之电源管理、鼠标和触控板、键盘和语言
    深度学习笔记(09)- 控制中心之时间设置
    深度学习笔记(08)- 控制中心之网络设置
    深度学习笔记(07)- 控制中心之声音设置
    深度学习笔记(06)- 控制中心之个性化设置
    深度学习笔记(05)- 控制中心之显示与默认程序
    深度学习笔记(04)- 控制中心之首页与用户管理
    深度学习笔记(03)- 启动器
  • 原文地址:https://www.cnblogs.com/yizhiamumu/p/16556153.html
Copyright © 2020-2023  润新知