当前位置:首页 » 《随便一记》 » 正文

Java基于Redis实现的分布式锁---电商下单案例

29 人参与  2022年11月07日 11:29  分类 : 《随便一记》  评论

点击全文阅读


一、案例

案例是一个简易版的电商项目中的下单功能,先简单分析下下单功能的一些基本步骤:
1.校验库存 2.保存订单 3.生成快照 4.扣减库存 5.删除购物车

二、问题

在这个案例中不难发现,在没有枷锁的情况下,如果两个线程同时执行下单操作,可能会在校验库存后同时执行后面的业务导致同时扣减库存,会引发库存小于0的情况,也就是超卖问题

在单体项目中,这个问题很容易解决,那就是加锁,使用一个 synchronized锁的可以很容易的解决

但是在分布式的项目中,你的后端服务器有多个,而 synchronized锁只能保证在同一个JVM中不会有并发问题,所以此时用 synchronized锁就不能解决问题了,此时就需要分布式锁

三、基于Redis实现分布式锁

1.实现

在这里插入图片描述
如上图,通过Redis的sexnx(key不在,添加;key在,不添加)来实现加锁,key设置为商品id,value暂时可以随便设置。在每次执行下单前,先使用sexnx来对商品加锁,如果对应的商品id在redis中不存在,那么该商品就是无锁的,可以加锁然后执行后序下单业务,如果对应商品id在redis中存在,那么说明该商品已经被加锁了,有其他线程正在操作这个商品,那么此时就需要等待或者直接下单失败了

代码实现:

 @Transactionalpublic String saveOrder(int productId) {    Boolean value = stringRedisTemplate.boundValueOps(productId + "").setIfAbsent("value");    if (value) {        try {            //检验库存            int stock = tbProductDao.querystock(productId);            if (stock > 0) {                //保存订单                System.out.println("保存订单");                //修改库存                tbProductDao.redustock(productId);                //删除购物车                return "订单提交成功";            } else {                return "商品不存在";            }        }catch (Exception e){            e.printStackTrace();        }finally {            //解锁            stringRedisTemplate.delete(productId+"");        }    }    return "订单提交失败";}

2. 优化

以上代码可以实现一个简单的分布式锁,但是仍然有一些可以优化的地方:
1.获取锁失败时会直接返回订单提交失败,应该不断尝试获取锁
2.无法保证锁的获取顺序是按照线程顺序来的

1)阻塞锁和非阻塞锁

对于问题1,可以通过阻塞锁或者非阻塞锁实现

阻塞锁:不断尝试获取锁,直到获取到锁为止

        //阻塞锁实现        while( b == false){            b = stringRedisTemplate.boundValueOps(productId + "").setIfAbsent("value");

非阻塞锁:如果获取不到锁就放弃,但是可以支持在一段时间内重试

 //非阻塞锁实现        int count = 1;        while( b == false && count < 4){            b = stringRedisTemplate.boundValueOps(productId + "").setIfAbsent("value");            count ++;        }

使用两种锁都可以实现
我们通过非阻塞锁来实现

2)公平锁和非公平锁

对于问题2 可以使用公平锁来实现
公平锁:按照线程的先后顺序获取锁
非公平锁:多个正在等待的线程随机获取锁

3.问题

1)线程异常导致无法释放锁

上述的分布式锁还有一个问题:
在执行业务的过程中,如果当前线程出现异常(宕机)没有释放锁,那么就会导致死锁

解决:
可以对锁设置过期时间,当出现异常没有释放锁,在过期时间结束时也能自动释放锁

2)T1过期释放T2锁

对锁设置过期时间能够解决死锁问题,但是另一个问题随之而来:假设有两个线程T1、T2,T1首先获得锁,但是在锁过期之前并没有完成业务的执行,同时T2线程成功加锁,T1执行结束后又释放锁(此时释放的是T2的锁),这就会导致T2在无锁状态下执行

解决:
在加锁时为锁设置唯一的value,释放锁时要先获取对应的value

 String value = UUID.randomUUID().toString(); boolean b = stringRedisTemplate.boundValueOps(productId + "")            .setIfAbsent(value,3,TimeUnit.MINUTES);

如果获取的指与当前value相同则释放锁

//查询操作String v = stringRedisTemplate.boundValueOps(productId + "").get();if(value.equals(v)){    //删除操作    stringRedisTemplate.delete(productId+"");}

简单来说就是每个线程只能释放自己加的锁

但是此时又产生了新的问题,看释放锁的代码,释放锁是分为两步进行的:1.查询value 2.删除
那么在这一步时也会产生并发问题,如果在查询完成后刚要删除锁,但是此时锁过期了并且其他线程成功加锁,那么也会导致本线程释放其他线程的锁

解决:
要解决这个问题本质是就是让查询和删除这两步操作是原子性的,可以使用lua脚本
lua:

if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end

配置Bean加载lua

@Beanpublic DefaultRedisScript<List> defaultRedisScript(){    DefaultRedisScript<List> defaultRedisScript = new DefaultRedisScript<>();    defaultRedisScript.setResultType(List.class);    defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));    return  defaultRedisScript;}

执行lua解锁

@AutoWiredprivate 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));

分析以上的问题不难看出,对于过期时间的设置是一个很困难的问题,必须保证在这个时间内业务能够执行完成并且也不能等待太久
对于这个问题可以使用看门狗机制

4)看门狗机制

看门狗线程工作原理:
监听当前线程锁的过期时间,当锁即将过期时如果有任务没有执行结束,则重置锁的过期时间,保证有任务线程正常执行的过程中,锁不会过期

4.Redisson

基于以上的问题,我们可以使用Redisson------一个基于Redis+看门狗机制的分布式锁框架

依赖:

<dependency>    <groupId>org.redisson</groupId>    <artifactId>redisson</artifactId>    <version>3.12.0</version></dependency>

配置yml

redisson:  addr:    singleAddr:      host: redis://localhost:6380      database: 0

RedissonConfig:

@Configurationpublic class RedissonConfig {    @Value("${redisson.addr.singleAddr.host}")    private String host;    @Value("${redisson.addr.singleAddr.database}")    private int database;    @Bean    public RedissonClient redissonClient(){        Config config = new Config();        config                .useSingleServer()                .setAddress(host)                .setDatabase(database);        return Redisson.create(config);    }}

使用:

@Transactionalpublic String saveOrder(int productId) {        //获取公平锁        RLock lock=redissonClient.getFairLock(productId+"");        //非阻塞锁        try {            boolean b = lock.tryLock(3, TimeUnit.MINUTES);            if(b){                System.out.println("业务执行");                return "订单提交成功";            }        } catch (InterruptedException e) {            e.printStackTrace();        }finally {            lock.unlock();        }        return "订单提交失败";}

点击全文阅读


本文链接:http://zhangshiyu.com/post/47211.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1