Redis分布式锁详解(Redisson)
Posted AdobePeng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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个人购买成功,其他人购买则返回商品已经卖完了
- 下面进行jmate操作,模拟1秒钟并发30个调用
- 按正常的逻辑来看,应该有20个人购买成功,还有10个人购买失败,接下来我们看下结果:
从结果中可以看出,每个人都消费成功,并且有多个人消费统一个商品。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)说到这里我们没有看效果
到这里一个简单的分布式锁已经完成,我们能够看出,这个里面我们要踩的坑还是挺多的。现在市面上也出现了不少框架,下面简单的介绍下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的工作原理:
Redisson通过LUA脚本和Redis进行交互,保证了原子性。解锁成功后会自动进行检测是否还持有锁,进行自动延时。
以上是关于Redis分布式锁详解(Redisson)的主要内容,如果未能解决你的问题,请参考以下文章