Redis高并发场景,如果直接去学会比较抓不住头绪,因此本文将一步步介绍Redis的高并发的步骤演进。
首先解释synchronized不适合在分布式场景,因为synchronized只适用自身的JVM,因此在分布式场景下多台机器的情况下,可能会出现同时操作一个key,从而会出现两个服务同时进行商品购买后,商品数量只减1的情况。
为了模拟分布式场景,模拟电商库存售卖的场景,每次调用接口相当于输出货物然后库存减1。
下面搭建一个简易的分布式系统。配置一个Nginx进行负载均衡、启动两个服务去连接Redis
Nginx主要配置如下
upstream redislock{
server 10.175.87.148:8080 weight=1;
server 10.175.87.148:8090 weight=1;
}
server{
listen 8000;
server_name localhost;
location / {
root html;
index index.html index.htm;
proxy_pass http://redislock;
}
}
接下来再开两个服务,服务主要提供对面提供操作Redis的接口。服务分别开启8090、8080端口
核心接口程序
@RequestMapping("/deduct_stock_syn")
public String deductStockSyn(){
//version 1
synchronized (this){ //synchronized 只在一个JVM进程中生效,分布式集群环境下不可以执行
//逻辑块
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else{
System.out.println("扣减失败,库存不足");
}
}
return "end";
}
为了测试出synchronized不适用再分布式场景,我们采用Jmeter进行压测
1、在Path上添加上请求的路径 2、配置压测参数,当前设置的是并发200次请求,循环执行4次
从结果上来看,两台服务上有相同的剩余库存,意味着货物ID为46的货物被售卖了两次,出现了超卖的情况。
采用Redis的SetNX命令
SETNX key value //存入一个不存在的字符串键值对,如果已经存在就不能设置了
使用该命令,可以很好的使用分布式场景下多线程的竞争,但是加锁容易,删锁就容易出问题,容易出现以下问题。
问题1、如果程序运行逻辑突然失控,陷入死循环,就不能主动删除锁,其他线程就获取不到该锁会一直阻塞住
问题2、如果程序运行中,系统突然宕机,也无法主动删除锁
针对问题1,可以在程序逻辑执行中添加 try、catch、finally 这类异常检测的内容,在finally中添加删除锁的操作,避免程序进入死循环后,其他线程无法拿到锁的情况。
针对问题2,添加锁的过期机制,在系统宕机后,锁自动过期,不影响其他用户获取该锁
@RequestMapping("/deduct_stock_setnx")
public String deductStockSetnx(){
//version 2
String lockKey = "lockKey";
String clientID = UUID.randomUUID().toString();
//逻辑块
try{
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "test"); //相当于 jdeis.setnx(key, value)
stringRedisTemplate.expire(lockKey, 30 , TimeUnit.SECONDS); //锁过期设置
if(!result){
return "error 1001";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else{
System.out.println("扣减失败,库存不足");
}
stringRedisTemplate.delete(lockKey);
}finally { //如果程序跑飞了删除key,但是在分布式环境下,可能出现自己的锁被别人删掉了,因为程序逻辑(锁失效的情况)
stringRedisTemplate.delete(lockKey);
}
return "end";
}
上述程序还存在问题
1、如果系统在程序刚加完锁后,就立马宕机,意味着还来不及设置过期机制,其他程序也获取不当该锁
2、如果程序的执行时间过长,超过了锁失效的时间,可能会出现锁失效的问题。锁失效具体是指,线程1执行时间过长,导致锁已经过期,这时候线程2获取到锁并运行程序,但是此时线程1执行结束主动释放锁,但是此时是线程2上的锁,因此线程3就会获取锁,从而导致了锁失效问题,配上我拙劣的图。
1、问题1的解决方法,是将获取锁指令和设置过期操作的指令和二为一,变成一条指令就具有原子性。
set key value [expiration EX seconds|PX milliseconds] [NX|XX]
参数说明:
EX seconds:将键的过期时间设置为 seconds 秒。
SET key value EX seconds 等同于 SETEX key seconds value
PX millisecounds:将键的过期时间设置为 milliseconds 毫秒。
SET key value PX milliseconds 等同于 PSETEX key milliseconds value
NX:只在键不存在的时候,才对键进行设置操作。
SET key value NX 等同于 SETNX key value
XX:只在键已经存在的时候,才对键进行设置操作
2、问题2的解决方法,是将获取的锁Value设置为当前客户端的ID,每次主动删除的时候先判断是否为自身的锁,如果不是自己加的锁就不能删除。除了这一种解决方案还有锁续命的办法,每次程序执行一段时间会自己是否还持有锁,如果持有就给锁续上时间。
@RequestMapping("/deduct_stock_setnx")
public String deductStockSetnx(){
//version 2
String lockKey = "lockKey";
String clientID = UUID.randomUUID().toString();
//逻辑块
try{
//Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "test"); //相当于 jdeis.setnx(key, value)
//stringRedisTemplate.expire(lockKey, 30 , TimeUnit.SECONDS);
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientID, 10, TimeUnit.SECONDS);//相当于set key value [expiration EX seconds|PX milliseconds] [NX|XX]
if(!result){
return "error 1001";
}
//解决方案: 锁续命
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else{
System.out.println("扣减失败,库存不足");
}
stringRedisTemplate.delete(lockKey);
}finally { //如果程序跑飞了删除key,但是在分布式环境下,可能出现自己的锁被别人删掉了,因为程序逻辑(锁失效的情况)
// stringRedisTemplate.delete(lockKey);
//添加这句,表示只删除自身的锁
if(clientID.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
采用无敌工具Redission,其底层是利用lua脚本实现的,Redission就包含锁续命的过程,使用起来十分方便。
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "product_101";
String clientId = UUID.randomUUID().toString();
RLock redissonLock = redisson.getLock(lockKey); //redisson 获取锁对象
try {
//加锁,实现续命操作
redissonLock.lock(); //setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//解锁命令
redissonLock.unlock();
/*if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}*/
}
运行程序在我的资源中下载