关于Redis分布式锁学习思考
分布式架构
windows配置nginx
配置nginx.conf
由于在本地模拟测试,所以我们做相同ip,不同端口的负载均衡。
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream sunpy {
server 127.0.0.1:8090 weight=1;
server 127.0.0.1:8091 weight=1;
}
server {
listen 8888;
server_name localhost;
location / {
proxy_pass http://sunpy;
}
}
}
启动nginx
nginx -c conf/nginx.conf
测试
不加分布式锁会出现的情况
public void testRedisLock(String shopCode) {
int amount = Integer.parseInt(stringRedisTemplate.opsForValue().get(shopCode));
if (amount <= 0) {
log.info("+++++++++++++++++++++++++++> 已经没有库存!");
} else {
int result = amount - 1;
stringRedisTemplate.opsForValue().set(shopCode, String.valueOf(amount - 1));
log.info("**************************> 当前库存为 = " + result);
}
}
压测结果:
问题:没有使用分布式锁,库存问题出现了超卖,出现了脏数据。
利用redis的setnx实现分布式锁
导包:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.x集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson-databind.version}</version>
</dependency>
程序:
@Slf4j
@Service
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void testRedisLock(String shopCode) {
String redisLock = "SPY-LOCK";
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(redisLock, "程序加锁测试", 10, TimeUnit.SECONDS);
if (!absent) {
log.info("===========================> 加锁操作失败!");
return;
}
try {
int amount = Integer.parseInt(stringRedisTemplate.opsForValue().get(shopCode));
if (amount <= 0) {
log.info("+++++++++++++++++++++++++++> 已经没有库存!");
} else {
int result = amount - 1;
stringRedisTemplate.opsForValue().set(shopCode, String.valueOf(result));
log.info("**************************> 当前库存为 = " + result);
}
} finally {
stringRedisTemplate.delete(redisLock);
}
}
}
压测结果:
Redis分布式锁会出现哪些问题
- 锁被其他人释放
- 死锁
- 锁提前过期
- 单点Redis出现故障
出现解其他人锁的问题
问题描述:
当前线程删除了其他线程的锁,主要还是在于锁获取的token令牌是相同的。
解决方法:
每次设置锁的时候,保持不一样,只有锁令牌相同允许通过,才可以删除锁。
public void testRedisLock(String shopCode) {
String redisLock = "SPY-LOCK";
String token = UUID.randomUUID().toString();
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(redisLock, token, 10, TimeUnit.SECONDS);
if (!absent) {
return;
}
try {
int amount = Integer.parseInt(stringRedisTemplate.opsForValue().get(shopCode));
if (amount <= 0) {
} else {
int result = amount - 1;
stringRedisTemplate.opsForValue().set(shopCode, String.valueOf(result));
log.info("**************************> 当前库存为 = " + result);
}
} finally {
String value = stringRedisTemplate.opsForValue().get(redisLock);
if (token.equals(value)) {
stringRedisTemplate.delete(redisLock);
}
}
}
死锁问题
问题描述:
当前线程在锁内,如果没有自己主动释放锁,出现异常等其他情况中断程序,没有运行释放锁的程序,会造成其他线程无法获取锁,而自己一直独占这种死锁情况。
解决方法:
提供一个redis锁过期时间,但是也会有问题,就是锁如果设置时间过短,程序未运行完就释放锁了,主要原因在于程序运行时间无法准确估计。
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(redisLock, "程序加锁测试", 10, TimeUnit.SECONDS);
Redisson框架
为什么使用Redisson框架
- Redisson框架本身提供了锁续期方案,优化了死锁问题;
- Redisson提供了重入锁机制;
- Redisson还提供了读写锁机制;
- Redisson提供了计数器和信号量(类似jdk中CountDownLatch和Semphore);
- 针对redis单点问题,Redisson提供了对Redis哨兵模式、集群模式支持的分布式锁;
springboot整合Redisson
导包:
<!--使用redisson作为分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.6</version>
</dependency>
获取RedissonClient的bean:
@Configuration
public class InitRedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private Integer redisPort;
@Value("${spring.redis.database}")
private Integer redisDatabase;
/**
* 所有对Redisson的使用都是通过RedissonClient对象
* @return
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(){
// 创建配置 指定redis地址及节点信息
Config config = new Config();
String redisAddr = "redis://" + redisHost + ":" + redisPort;
config.useSingleServer()
.setAddress(redisAddr)
.setPassword(null)
.setDatabase(redisDatabase);
// 根据config创建出RedissonClient实例
return Redisson.create(config);
}
}
分布式锁减库存:
public void testRedission(String shopCode) {
String redisLock = "SPY-LOCK";
RLock rLock = redissonClient.getLock(redisLock);
try {
rLock.lock(10, TimeUnit.SECONDS);
int amount = Integer.parseInt(stringRedisTemplate.opsForValue().get(shopCode));
if (amount <= 0) {
log.info("**************************> 没有库存了");
} else {
int result = amount - 1;
stringRedisTemplate.opsForValue().set(shopCode, String.valueOf(result));
log.info("**************************> 当前库存为 = " + result);
}
} finally {
rLock.unlock();
}
}
结果:
redisson的非阻塞锁
利用redission中的tryLock方法,实现非阻塞锁。
public void testRedission(String shopCode) {
String redisLock = "SPY-LOCK";
RLock rLock = redissonClient.getLock(redisLock);
try {
boolean flag = rLock.tryLock(10, TimeUnit.SECONDS);
if (!flag) {
log.info("++++++++++++++++++++++++++> 没有获取到锁");
return;
}
int amount = Integer.parseInt(stringRedisTemplate.opsForValue().get(shopCode));
if (amount <= 0) {
log.info("**************************> 没有库存了");
} else {
int result = amount - 1;
stringRedisTemplate.opsForValue().set(shopCode, String.valueOf(result));
log.info("**************************> 当前库存为 = " + result);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
}
redisson自动续期
实现:
boolean flag = rLock.tryLock(10, 30, TimeUnit.SECONDS);
10:waitTime,等待锁的时间,在这个指定时间内无法获取锁返回失败。
30:leaseTime,锁持有时间,到达指定时间了,锁未持有了,那么锁会释放。
TimeUnit.SECONDS:时间单位
参考
怎样实现redis分布式锁? - Kaito的回答 - 知乎
https://www.zhihu.com/question/300767410/answer/1931519430