Redis分布式锁详解(Redisson)

本章节内容讲解通过Redis自带API实现分布式锁以及了解Redisson框架如何实现分布式锁

1、首先我们来看一下,在分布式系统中不加分布式锁的抢购活动中会发生什么:

1)前置条件

  • 在本地启动两个服务,为了方便这里启用了两个Spring Boot工程
  • 启用Nginx,对外提供端口号9999,负载到本地的两个服务
  • 使用Jmate工具,模拟高并发场景
  • 在Redis中存入某商品的数量

 2)开始撸代码

  • 如下所示:对外提供一接口(buyOneIndex),用户每点击一次,调用Redis工具类进行商品数量减一操作。两个工程代码相同,对外提供的端口号分别为8080、8081
  • @RestController
    public class MainController {
       @Autowired
       private RedisTool redisTool;
       /**
        * 对外提供的调用接口
        */
       @RequestMapping("buyOneIndex")
       public void buyOneIndex(){
          //调用redis工具类中的方法,模拟购买场景,购买一次,减少一个
          redisTool.reduceOne();
       }
    }

     

  • @Component
    public class RedisTool {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        /**
         * 模拟购买场景,购买一个,将redis中对应的数量减少一个
         */
        public void reduceOne(){
            String key = "num";
            //获取redis中的数量
            String value = stringRedisTemplate.opsForValue().get( key );
            //如果当前数量>0,则能够购买,并且将减一后的结果塞回redis
            if(Integer.valueOf( value )>0){
                int num = Integer.valueOf( value ) - 1;
                System.out.println("消费1个商品,还剩:"+num+"个");
                stringRedisTemplate.opsForValue().set( key,String.valueOf( num ) );
                System.out.println("消费1个商品,还剩:"+num+"个");
            }else {
                System.out.println("商品已经卖完...");
            }
        }
    }

     

  • 接下来看下Nginx配置(后续会进行更新Nginx的详细操作):对外提供统一端口号9999,负载到上面的两个工程(8080、8081)上
  • 
    #nginx进程数,建议设置为等于CPU总核心数。可以和worker_cpu_affinity配合
    worker_processes  1;
    events {
        worker_connections  1024;
    }
    http {
        include       mime.types;
        default_type  application/octet-stream;
        sendfile        on;
        charset utf-8;
        upstream pzfdemo{
        	server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=30s;
        	server 127.0.0.1:8081 weight=1 max_fails=2 fail_timeout=30s;
        }
    
    
        server {
            listen       9999;
            server_name  localhost;
            location / {
            		proxy_pass http://pzfdemo;
            		proxy_set_header Host $host;
                root   html;
                index  index.html index.htm;
            }
        }
    }
    

 

  • Redis中我们存放某一个商品的数量为20个,也就是该活动只放出20个商品,每个人购买一个商品,也就是说应该只有20个人购买成功,其他人购买则返回商品已经卖完了
  • Redis分布式锁详解(Redisson)_第1张图片

 

  •  下面进行jmate操作,模拟1秒钟并发30个调用
  • Redis分布式锁详解(Redisson)_第2张图片
  •  Redis分布式锁详解(Redisson)_第3张图片

 

  • 按正常的逻辑来看,应该有20个人购买成功,还有10个人购买失败,接下来我们看下结果:
  • Redis分布式锁详解(Redisson)_第4张图片
  • Redis分布式锁详解(Redisson)_第5张图片
  • Redis分布式锁详解(Redisson)_第6张图片
  •  

 从结果中可以看出,每个人都消费成功,并且有多个人消费统一个商品。30个人都抢结束了,Redis中还有14个。很显然这个结果是有问题的,当我发货的时候,只有20件商品,我要发给30个人,老板估计脸要绿了。

3)下面我们来写一把Redis的分布式锁,如下:

在写分布式锁中需要考虑很多问题

  • 加上锁了,线程结束会不会不释放?
  • 加上锁了,线程结束释放的是不是当前线程加的锁?
  • 加上锁了,线程还没有结束,进程被杀或者系统宕机,没有释放锁怎么办?
  • 给锁加上自动失效时间,加多久合适?
@Component
public class RedisTool {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 模拟购买场景,购买一个,将redis中对应的数量减少一个
     */
    public void reduceOne(){
        String key = "num";
        //加锁的key
        String lockKey = "lockKey";
        //加锁的value,用于后续释放锁时做判断,只有当前线程能够释放当前线程所加的锁
        String logvalue = UUID.randomUUID().toString();
        try {
            //由于Redis同一个key只能设置一次,所以通过该原则来设置锁
            //boolean result = stringRedisTemplate.opsForValue().setIfAbsent( lockKey,logvalue );
            //stringRedisTemplate.expire( lockKey,10, TimeUnit.SECONDS );
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent( lockKey, logvalue, 30, TimeUnit.SECONDS );
            //如果为false,说明该线程没有争抢到锁
            if(!aBoolean){
                System.out.println("系统繁忙,请重试");
                return;
            }
            //获取redis中的数量
            String value = stringRedisTemplate.opsForValue().get( key );
            //如果当前数量>0,则能够购买,并且将减一后的结果塞回redis
            if(Integer.valueOf( value )>0){
                int num = Integer.valueOf( value ) - 1;
                System.out.println("消费1个商品,还剩:"+num+"个");
                stringRedisTemplate.opsForValue().set( key,String.valueOf( num ) );
            }else {
                System.out.println("商品已经卖完...");
            }
        }finally {
            //当前线程只删除当前线程所持有的锁
            if(logvalue.equals( stringRedisTemplate.opsForValue().get( lockKey ) )){
                stringRedisTemplate.delete( lockKey );
            }
        }

    }
}

 代码如上,下面解释一下上面的问题

  • 加上锁了,线程结束会不会不释放?

       这里通过加上finally,在finally代码块中对锁进行释放

  • 加上锁了,线程结束释放的是不是当前线程加的锁?

       这里通过给锁附上value值,这个value值通过UUID来设置,重复的可能性几乎为0,所以保证了当前线程执行结束释放的当前线程所加的锁

  • 加上锁了,线程还没有结束,进程被杀或者系统宕机,没有释放锁怎么办?

       这里就需要给锁设置自动失效时间,代码中为了保证原子性,在setIfAbsent方法中直接设置过期时间

  • 给锁加上自动失效时间,加多久合适?

       这个一个根据具体业务来设置,是10秒还是30秒。但是如果到时间了,该线程还是没有执行结束,那么锁就被自动释放掉,这是需要写一个定时器来扫描。例如我设置了30秒,那么我设置每隔10秒钟来扫描一次,看看当前锁是否还持有,如果持有,就对其进行时间重置操作

4)说到这里我们没有看效果

Redis分布式锁详解(Redisson)_第7张图片

 Redis分布式锁详解(Redisson)_第8张图片

 

到这里一个简单的分布式锁已经完成,我们能够看出,这个里面我们要踩的坑还是挺多的。现在市面上也出现了不少框架,下面简单的介绍下Redisson框架,使得Redis分布式实现的更为简单,而且封装的较为完好。上代码:

@Component
public class RedisTool {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redisson;
    /**
     * 模拟购买场景,购买一个,将redis中对应的数量减少一个
     */
    public void reduceOne(){
        String key = "num";
        String lockKey = "lockKey";
        RLock lock = redisson.getLock( lockKey );
        try{
            lock.lock();
            //获取redis中的数量
            String value = stringRedisTemplate.opsForValue().get( key );
            //如果当前数量>0,则能够购买,并且将减一后的结果塞回redis
            if(Integer.valueOf( value )>0){
                int num = Integer.valueOf( value ) - 1;
                System.out.println("消费1个商品,还剩:"+num+"个");
                stringRedisTemplate.opsForValue().set( key,String.valueOf( num ) );
            }else {
                System.out.println("商品已经卖完...");
            }
        }finally {
            lock.unlock();
        }
    }
}

实现的效果和之前的效果是一样的,下面通过一张图来展示一下Redisson的工作原理:

Redis分布式锁详解(Redisson)_第9张图片

Redisson通过LUA脚本和Redis进行交互,保证了原子性。解锁成功后会自动进行检测是否还持有锁,进行自动延时。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Redis分布式锁详解(Redisson))