前言
本文分享的是基于Redisson分布式锁的实战以及我们自己如何大致利用Redis实现分布式锁。
一、什么是分布式锁?
首先我们要搞清楚的就是什么是分布式锁,大家广而为知的可能是锁。像什么lock锁,synchronized锁等。那这些锁我们通常都称为JVM内部锁或者进程锁。我们知道java程序开发完成以后都是以jar或war包的形式运行,那可以这样理解,在服务器上运行一次jar或war包就会产生一个与之对应的进程。而lock锁和synchronized锁只能在一个进程内有效,不同的进程之间是不起作用的。下面我来描述一个应用场景:
比如说我们现在有一个商品需要秒杀。在没有秒杀以前,我们的架构是这样的。
没有秒杀前的架构(这种场景完全使用jvm进程锁没有问题):
秒杀时的架构(这种场景必须使用分布式锁):
很明显,平时流量可能不怎么大,我们库存服务部署一台就足够了,但是在秒杀时刻,一台可能扛不住。需要横向扩展。这时候我们就需要考虑使用分布式锁了。因为单纯的JVM进程锁只能在某一台库存服务的内部进行控制,这种场景下只能使用分布式锁来解决并发带来的问题。当然,分布式锁的实现还可以使用别的。例如zookeeper。今天主要分享redis。
二、Redis锁的实现
之前分享的文章有关于redis可以实现分布式锁的功能。其实就是一条命令。
setnx key value //如果返回1说明key不存在,加锁成功,否则,加锁失败返回0
这里我简单的通过代码还原一下上面的场景:
Integer stock001 = Integer.parseInt(redisTemplate.opsForValue().get("strock_001"));
if (stock001 > 0) {
int realStock001 = stock001 - 1;
redisTemplate.opsForValue().set("strock_001", realStock001 + "");
System.out.println("减库存成功,剩余商品数量 " + realStock001);
} else {
System.out.println("减库存失败,库存不足");
}
上面这段代码就是一段减库存的代码。没有加任何的锁。如果是单机器部署这段程序,我们只需要在代码的外围加上synchronized锁或者lock锁都是没有问题的。我就不测试了。这里主要说明分布式环境下是怎么实现的(多台机器部署这个程序)
还是通过代码来说明问题:
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping(value = "/deduck_stock")
public String index() {
String lockKey = "product_001";
//为了防止锁超时老删除别人的锁,可以设置一个版本,删之前看看是否是自己加的锁
String clientId = UUID.randomUUID().toString();
try {
//boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"001");
//redisTemplate.expire(lockKey,30, TimeUnit.SECONDS);//防止系统宕机,造成永久死锁,加一超时时间
//因为上面两条命令不具备原子性,如果第一条命令加锁成功之后,系统出现异常,这时候就会造成死锁。
boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
if(!result){
return "未获取锁";
}
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
redisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//释放锁之前先判断是否是自己加的锁
if(clientId.equals(redisTemplate.opsForValue().get(lockKey))){
//加异常是因为怕业务代码执行过程中出现异常而导致最终锁未能释放造成资源死锁
redisTemplate.delete(lockKey);
}
}
return "end";
}
上面的这段代码就是一个分布式锁的实现过程了(里面的注释都是前辈们踩过的坑,一步一步修改过的,需要认真看)。但是还是有一点的瑕疵。就是超时时间的问题,超时时间设置多少合适,好像设置多少都不合适,设置30s,有时候因为机器问题,可能你的业务代码执行时间超过了你设置的时间,这就会导致锁提前释放,别的线程就可以获取锁成功,这就跟没有加锁一样。所以,为了防止这种事情发生,我们需要给锁进行续命,当主线程获取锁成功的时候,主线程接可以执行业务代码了,此时,我们在开启一个子线程,子线程用来定时轮询检查主线程的超时时间并判断当前锁有没有释放,如果没有释放并且超时时间小于1/3(这个时间自己可以随便设置)的时间,那子线程就给主线程的key的过期时间还原为30s,这样循环往复,直到主线程执行完业务代码,主动释放锁为止。当然,市面上现在已经有非常成熟的分布式的解决的方案了,就是Redisson,就是基于redis实现的了。
备注:上面这段代码在超高并发的情况下还是有问题的。
三、Redisson的实战
redisson在redis的基础上已经帮我们实现了一把非常健壮的分布式锁。
其底层大量应用了Lua脚本。Lua脚本是用C语言实现的。Lua脚本最大的特点就是具备原子性。一般在Redis中替代Redis自身的事务特性。接下来看一下如何在项目使用Redisson。
首先需要引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
需要将Redisson交给spring容器管理:
@Bean
public Redisson redisson(){
Config config = new Config();
//单机模式
config.useSingleServer().setAddress("redis://192.168.2.28:6379").setDatabase(0);
//集群模式
// config.useClusterServers()
// .addNodeAddress("redis://192.168.2.28:6379")
// .addNodeAddress("redis://192.168.2.29:6379")
// .addNodeAddress("redis://192.168.2.30:6379");
return (Redisson)Redisson.create(config);
}
业务代码中引用:
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping(value = "/deduck_stock")
public String index() {
String lockKey = "product_001";
RLock redissonLock = redisson.getLock(lockKey);
//为了防止锁超时老删除别人的锁,可以设置一个版本,删之前看看是否是自己加的锁
String clientId = UUID.randomUUID().toString();
try {
//boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"001");
//redisTemplate.expire(lockKey,30, TimeUnit.SECONDS);//防止系统宕机,造成永久死锁,加一超时时间
//因为上面两条命令不具备原子性,如果第一条命令加锁成功之后,系统出现异常,这时候就会造成死锁。
// boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
// if(!result){
// return "未获取锁";
// }
redissonLock.lock();
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
redisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
redissonLock.unlock();
//释放锁之前先判断是否是自己加的锁
// if(clientId.equals(redisTemplate.opsForValue().get(lockKey))){
// //加异常是因为怕业务代码执行过程中出现异常而导致最终锁未能释放造成资源死锁
// redisTemplate.delete(lockKey);
// }
}
return "end";
}
其实,上面在描述超时时间的那段叙述就是Redisson的实现原理。
四、测试是否实现了分布式锁
这里先声明一下测试环境:
1 、nginx 环境(因为要部署多个jar包,需要借助nginx做负载均衡)
nginx安装可参考:https://www.cnblogs.com/lishen2021/p/14676143.html
2、jmeter压测工具
测试步骤:
1、不应用程序部署到两台机器上(这里我就以不同的端口号启动)
端口号分别是9003和9004启动:
2、需要在nginx中配置代理转发
3、jmeter准备压测
下载地址:https://jmeter.apache.org/download_jmeter.cgi
设置压测参数和地址:
点击上面的绿色三角形测试(现在redis中设置一个库存):
set stock 50
测试结果:
五、总结
今天主要是redis分布式锁的实战。其底层就是setnx命令的应用。重点理解分布式锁和JVM进程锁。