redis-分布式锁

分布式锁

1.分布式并发问题

提交订单: 商品超卖问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fU3kEKL0-1670672259386)(https://gitee.com/ajiemo/gic/raw/blogs/20221210191515.png)]

2.如何解决分布式并发问题呢 ?

使⽤redis实现分布式锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfgrnvzq-1670672259387)(https://gitee.com/ajiemo/gic/raw/blogs/20221210191630.png)]

在进行添加之前就向redis中添加一个 key为 skuId 的数据 完成操作之后就删除 数据 实现锁的机制

  • 1.如果订单中部分商品加锁成功,但是某⼀个加锁失败,导致最终加锁状态失败——需要对已经锁定的部分商品释放锁

  • 2.在成功加锁之前,我们根据购物⻋记录的id查询了购物⻋记录(包含商品库存),能够直接使⽤这个库存进⾏库存校验?

    • ——不能,因为在查询之后加锁之前可能被并发的线程修改了库存;因此在进⾏库存⽐较之前需要重新查询库存。

代码如下:

 @Override
    @Transactional()
    public Map<String, String> addOrder(String cids, Orders order) throws SQLException {
        HashMap<String, String> map = new HashMap<>();

        //根据cids查询当前订单中关联的购物车详情 和库存
        String[] arr = cids.split(",");
        List<Integer> cidsList = new ArrayList<>();
        for (String s : arr) {
            cidsList.add(Integer.parseInt(s));
        }
        List<ShoppingCartVO> list = shoppingCartMapper.selectShopcartByCids(cidsList);

        //从购物车信息中获取到要购买的skuId   以id为key写道redis中
        boolean isLock = true;
        //记录已经存入redis中的id
        String[] skuIds = new String[list.size()];
        for (int i = 0; i < list.size(); i++) {
            String skuId = list.get(i).getSkuId();
            //设置时间是防止在加完锁之后 线程出现故障 导致锁不能释放
            Boolean ifAbsent = stringRedisTemplate.boundValueOps(skuId).setIfAbsent("随便", 10, TimeUnit.SECONDS);
            if (ifAbsent) {
                skuIds[i] = skuId;
                values.put(skuId, value);
            }
            isLock = isLock && ifAbsent;
        }

        //加锁
        if (isLock) {
            
            //校验库存
            //保存订单
            //扣减库存 
            //删除购物车 购买成功之后 购物车自动删除
            
            //释放锁
            for (String skuId : skuIds) {
                if (skuId != null && !"".equals(skuId)) {
                    stringRedisTemplate.delete(skuId);
                }
            }
        } else {
            //释放锁
            for (String skuId : skuIds) {
                if (skuId != null && !"".equals(skuId)) {
                    stringRedisTemplate.delete(skuId);
                }
            }
        }
        return null;
    }

问题:

当当前线程加锁成功之后,执⾏添加订单的过程中,如果当前线程出现异常导致⽆法释放锁,这个问题⼜该如何解决呢?

3.解决因线程异常导致⽆法释放锁的问题

在对商品进⾏加锁时,设置过期时间,这样⼀来及时线程出现故障⽆法释放锁,在过期时间结束时也会⾃动“释放锁”

Boolean ifAbsent = stringRedisTemplate.boundValueOps(skuId).setIfAbsent(value, 10, TimeUnit.SECONDS);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQy4EX5D-1670672259389)(https://gitee.com/ajiemo/gic/raw/blogs/20221210192821.png)]

问题:

当给锁设置了过期时间之后,如果当前线程t1因为特殊原因,在锁过期前没有完成业务执⾏,将会释放锁,同时其他线程(t2)就可以成功加锁了,当t2加锁成功之后,t1执⾏结束释放锁就会释放t2的锁,就会导致t2在⽆锁状态下执⾏业务。

4.解决因t1过期释放t2锁的问题

  • 在加锁的时候,为每个商品设置唯⼀的value

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yicm1sNF-1670672259390)(https://gitee.com/ajiemo/gic/raw/blogs/20221210193258.png)]

  • 在释放锁的时候,先获取当前商品在redis中对应的value,如果获取的值与当前value相同,则释放锁

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dk13WipQ-1670672259391)(https://gitee.com/ajiemo/gic/raw/blogs/20221210193430.png)]

问题:

当释放锁的时候,在查询并判断“这个锁是当前线程加的锁”成功之后,正要进⾏删除时锁过期了,并且被其他线程成功加锁,⼀样会导致当前线程删除其他线程的锁。

  • Redis的操作都是原⼦性的
  • 要解决如上问题,必须保证查询操作和删除操作的原⼦性——使⽤lua脚本

使⽤lua脚本

  • 在resources⽬录下创建unlock.lua,编辑脚本:

    if redis.call("get",KEYS[1]) == ARGV[1] then
    
     return redis.call("del",KEYS[1])
    
    else
    
     return 0
    
    end
    
  • 配置Bean加载lua脚本

    @Bean
    
    public DefaultRedisScript<List> defaultRedisScript(){
    
     DefaultRedisScript<List> defaultRedisScript = new
    
    DefaultRedisScript<>();
    
     defaultRedisScript.setResultType(List.class);
    
     defaultRedisScript.setScriptSource(new ResourceScriptSource(new
    
    ClassPathResource("unlock.lua")));
    
     return defaultRedisScript;
    
    }
    
  • 通过执⾏lua脚本解锁

    
    @AutoWired
    
    private DefaultRedisScript defaultRedisScript;
    
    //执⾏lua脚本
    
    List<String> keys = new ArrayList<>();
    
    keys.add(skuId);
    
    List rs = stringRedisTemplate.execute(defaultRedisScript,keys ,
    
    values.get(skuId));
    
    System.out.println(rs.get(0));
    
    
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值