Redis实现高并发分布式锁

246 阅读4分钟

分布式锁场景

在分布式环境下多个操作需要以原子的方式执行

首先启一个

springboot

项目,再引入
redis
依赖包:

<!-- https://mvnrepository.com/artifa ... -starter-data-redis -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

<version>2.2.2.RELEASE</version>

</dependency>

以下是一个扣减库存的接口作为例子:

@RestController

public class IndexController {

@Autowired

private StringRedisTemplate stringRedisTemplate

@RequestMapping("/deduct_stock")

public Stirng deductStock() {

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)

if (stock > 0) {

int realStock = stock - 1;

stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)

System.out.println(

扣减成功,剩余库存:
" + realStock + "");

} else {

System.out.println(

扣减失败,库存不足!
" );

}

return "end";

}

}

1.

单实例应用场景

以上代码使用

JMeter

压测工具进行调用,设置参数为:

Number Of Threads[users]:100

Ramp Up Period[in seconds]:0

Loop Count

2

用单个

web

调用,结果出现并发问题:

解决方案:加入同步锁(

synchronized

@RestController

public class IndexController {

@Autowired

private StringRedisTemplate stringRedisTemplate

@RequestMapping("/deduct_stock")

public Stirng deductStock() {

synchronized(this) {

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)

if (stock > 0) {

int realStock = stock - 1;

stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)

System.out.println(

扣减成功,剩余库存:
" + realStock + "");

} else {

System.out.println(

扣减失败,库存不足!
" );

}

return "end";

}

}

}

2.

多实例分布式场景

以上代码,比如有多个应用程序,用

nginx

做负载均衡,进行同时调用压测

两个程序存在同样的扣减,出现并发现象。

第一个应用扣减结果显示:

第二个应用扣减结果显示:

解决方案:

redis

setnx
方法(可参考
SETNX
api

多个线程

setnx

调用时,有且仅有一个线程会拿到这把锁,所以拿到锁的执行业务代码,最后释放掉锁,代码如下:

@RestController

public class IndexController {

@Autowired

private StringRedisTemplate stringRedisTemplate

@RequestMapping("/deduct_stock")

public Stirng deductStock() {

String lockkey = "lockkey";

Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx

if(!result) {

return "";

}

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)

if (stock > 0) {

int realStock = stock - 1;

stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)

System.out.println(

扣减成功,剩余库存:
" + realStock + "");

} else {

System.out.println(

扣减失败,库存不足!
" );

}

springRedisTemplate.delete(lockkey);

return "end";

}

}

调用

200

次,压测结果显示还是有问题,只减掉了一部分:

这时,加大压测次数,结果正常了:

第一个应用扣减结果显示:

第二个应用扣减结果显示:

这个只是因为加大了调用次数,执行业务代码需要一点时间,这段时间拒绝了很多等待获取锁的请求。但是,还是有问题,假如

redis

服务挂掉了,抛出异常了,这时锁不会被释放掉,出现死锁问题,可以添加
try catch
处理,代码如下:

@RestController

public class IndexController {

@Autowired

private StringRedisTemplate stringRedisTemplate

@RequestMapping("/deduct_stock")

public Stirng deductStock() {

String lockkey = "lockkey";

try{

Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx

if(!result) {

return "";

}

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)

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{

springRedisTemplate.delete(lockkey);

}

return "end";

}

}

这时,

Redis

服务挂掉导致死锁的问题解决了,但是,如果服务器果宕机了,又会导致锁不能被释放的现象,所以可以设置超时时间为
10s
,代码如下:

@RestController

public class IndexController {

@Autowired

private StringRedisTemplate stringRedisTemplate

@RequestMapping("/deduct_stock")

public Stirng deductStock() {

String lockkey = "lockkey";

try{

Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue",10,TimeUnit.SECONDS);//jedis.setnx

//Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx

//stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);

if(!result) {

return "";

}

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)

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{

springRedisTemplate.delete(lockkey);

}

return "end";

}

}

这时,如果有一个线程执行需要

15s

,当执行到
10s
时第二个线程进来拿到这把锁,会出现多个线程拿到同一把锁执行,在第一个线程执行完时会释放掉第二个线程的锁,以此类推…就会导致锁的永久失效。所以,只能自己释放自己的锁,可以给当前线程取一个名字,代码如下:

@RestController

public class IndexController {

@Autowired

private StringRedisTemplate stringRedisTemplate

@RequestMapping("/deduct_stock")

public Stirng deductStock() {

String lockkey = "lockkey";

String clientId = UUID.randomUUID().toString();

try{

Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,clientId ,10,TimeUnit.SECONDS);//jedis.setnx

//Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx

//stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);

if(!result) {

return "";

}

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)

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{

springRedisTemplate.delete(lockkey);

}

return "end";

}

}

永久失效的问题解决了,但是,如果第一个线程执行

15s

,还是会存在多个线程拥有同一把锁的现象。所以,需要续期超时时间,当一个线程执行
5s
后对超时时间进行续期都
10s
,就可以解决了,续期设置可以借助
redission
工具。

Redission

使用

Redission

分布式锁实现原理:

pom.xml

<dependency>

<groupId>org.redisson</groupId>

<artifactId>redisson</artifactId>

<version>3.6.5</version>

</dependency>

Application.java

启动类

@bean

public Redission redission {

//

此为单机模式

Config config = new Config();

config.useSingleServer().setAddress("redis://120.0.0.1:6379").setDatabase(0);

return (Redission)Redission.creat(config);

}

最终解决以上所有问题的代码如下:

@RestController

public class IndexController {

@Autowired

private StringRedisTemplate stringRedisTemplate

@Autowired

private Redissionredission

@RequestMapping("/deduct_stock")

public Stirng deductStock() {

String lockkey = "lockkey";

//String clientId = UUID.randomUUID().toString();

RLock lock = redission.getLock();

try{

//Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,clientId ,10,TimeUnit.SECONDS);//jedis.setnx

//Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx

//stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);

//

加锁
:redission
默认超时时间为
30s
,每
10s
续期一次,也可以自己设置时间

lock.lock(60,TimeUnit.SECONDS);

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)

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{

lock.unlock();

//springRedisTemplate.delete(lockkey);

}

return "end";

}

}

高并发分布式锁的问题得到解决。