参考
深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!
尚硅谷2022最新版分布式锁系列视频
快速上手
三种实现
redis、zookeeper、数据库(MySQL)
JMeter
redis
本地Redis中模拟库存扣减的过程。
版本一
@GetMapping("sell")
public String sell() {
int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
if (sell > 0) {
int realStock = sell - 1;
stringRedisTemplate.opsForValue().set("sell", realStock + "");
System.out.println(port + ":扣除成功,剩余库存" + realStock);
} else {
System.out.println(port + ":扣减失败,库存不足");
}
return port + "end";
}
出现了重复扣减,我们进行改进。
版本二
@GetMapping("sell2")
public String sell2() {
synchronized (this) {
int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
if (sell > 0) {
int realStock = sell - 1;
stringRedisTemplate.opsForValue().set("sell", realStock + "");
System.out.println(port + ":扣除成功,剩余库存" + realStock);
} else {
System.out.println(port + ":扣减失败,库存不足");
}
return port + "end";
}
}
synchronized 无法跨jvm进行锁定,当有多台机器时,需要使用分布式锁来保证其正确性。
版本三
使用 Redis 的 setIfAbsent
public String sell3() {
String lock = "lock";
//加锁
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, "this is lock");
if (result) {
int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
if (sell > 0) {
int realStock = sell - 1;
stringRedisTemplate.opsForValue().set("sell", realStock + "");
System.out.println(port + ":扣除成功,剩余库存" + realStock);
} else {
System.out.println(port + ":扣减失败,库存不足");
}
//删除锁
stringRedisTemplate.delete(lock);
return port + "end";
}
return "获取锁错误";
}
加锁部分如果出现异常会导致锁无法释放。
版本四
使用 try catch来保证程序内异常仍然可以释放锁。
public String sell4() {
String lock = "lock";
try {
boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, "this is lock");
if (result) {
int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
if (sell > 0) {
int realStock = sell - 1;
stringRedisTemplate.opsForValue().set("sell", realStock + "");
System.out.println(port + ":扣除成功,剩余库存" + realStock);
} else {
System.out.println(port + ":扣减失败,库存不足");
}
} else {
return port + "获取锁错误";
}
} finally {
stringRedisTemplate.delete(lock);
}
return port + "end";
}
虽然程序内异常可以屏蔽,但是如果强制结束进程仍会造成死锁。
版本五 增加过期时间
注意事项:加锁的动作应该是原子性操作,如果分为两步,在第一步结束后宕机仍然存在死锁的问题。
boolean result = stringRedisTemplate.opsForValue()
.setIfAbsent(lock, "this is lock", 30, TimeUnit.SECONDS);
假设过期时间为10s,业务执行10.1s,存在误删除别人锁的情况。
版本六 增加唯一标识
通过 uuid 来确保删除前判断是否自己持有锁。
@GetMapping("sell6")
public String sell6() {
String lock = "lock";
String uuid = UUID.randomUUID().toString();
try {
boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 10, TimeUnit.SECONDS);
if (result) {
int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
if (sell > 0) {
int realStock = sell - 1;
stringRedisTemplate.opsForValue().set("sell", realStock + "");
System.out.println(port + ":扣除成功,剩余库存" + realStock);
} else {
System.out.println(port + ":扣减失败,库存不足");
}
} else {
return port + "获取锁错误";
}
} finally {
if (uuid.equals(stringRedisTemplate.opsForValue().get(lock))) {
//
//
stringRedisTemplate.delete(lock);
}
}
return port + "end";
}
由于删除锁不是原子性,所以删除锁错误代码如下所示
//线程一
if (uuid.equals(stringRedisTemplate.opsForValue().get(lock))) {
//锁到期了
//线程二
stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 10, imeUnit.SECONDS);
//线程一
stringRedisTemplate.delete(lock);
}
版本七 使用lua脚本释放锁 (原子性)
查询key对应的值,如果和uuid相等删除key,否则结束操作。
del.lua
local key = KEYS[1]
local val = redis.call('GET',key)
if val == ARGV[1]
then
redis.call('DEL', KEYS[1])
return 1
else
return 0
end
RedisScriptConfig.java
@Configuration
public class RedisScriptConfig {
@Bean
public DefaultRedisScript<Boolean> redisScript() {
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
ClassPathResource classPathResource = new ClassPathResource("lua/del.lua");
ResourceScriptSource resourceScriptSource = new ResourceScriptSource(classPathResource);
script.setResultType(Boolean.class);
script.setScriptSource(resourceScriptSource);
return script;
}
}
@GetMapping("sell7")
public String sell7() {
String lock = "lock";
String uuid = UUID.randomUUID().toString();
try {
boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 10, TimeUnit.SECONDS);
if (result) {
int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
if (sell > 0) {
int realStock = sell - 1;
stringRedisTemplate.opsForValue().set("sell", realStock + "");
System.out.println(port + ":扣除成功,剩余库存" + realStock);
} else {
System.out.println(port + ":扣减失败,库存不足");
}
} else {
return port + "获取锁错误";
}
} finally {
List<String> keys = Collections.singletonList(lock);
stringRedisTemplate.execute(redisScript, keys, uuid);
}
return port + "end";
}
仔细思考一下,锁的过期时间要充分冗余,避免提前过期的风险。
版本八 redisson
pom依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.8</version>
</dependency>
RedissionConfig
@Configuration
public class RedissionConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
if (ObjectUtils.isEmpty(password)){
config.useSingleServer().setAddress("redis://" + host + ":" + port);
}
else{
config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
}
return Redisson.create(config);
}
}
controller
@Autowired
private RedissonClient redissonClient;
@GetMapping("sell9")
public String sell9() {
//获取锁
RLock rLock = redissonClient.getLock("lock");
try {
//加锁
rLock.lock();
int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
if (sell > 0) {
int realStock = sell - 1;
stringRedisTemplate.opsForValue().set("sell", realStock + "");
System.out.println(port + ":扣除成功,剩余库存" + realStock);
} else {
System.out.println(port + ":扣减失败,库存不足");
}
} finally {
//解锁
rLock.unlock();
}
return port + "end";
}
版本九 自旋
redisson同样为我们提供了自旋机制
RLock rLock = redissonClient.getSpinLock(lock);
自旋和非自旋的区别
redis作为分布式锁的主要问题
过期时间、原子性、唯一标识、异常等问题。
redis集群存在的问题
当 redis 处于集群的时候会存在各种各样的极端情况,对于资源绝对正确的业务,仍需要采取其他手段来保证数据正确,也就是不能认为redis做分布式锁是一定可靠的。
Zookeeper (一致性)
Spring Boot使用Zk实现分布式锁
引入 Apache Curator
Apache Curator 是分布式协调服务Apache ZooKeeper的 Java/JVM 客户端库。它包括一个高级 API 框架和实用程序,使 Apache ZooKeeper 的使用更加轻松和可靠
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.1</version>
</dependency>
新建配置类负责连接Zk
@Configuration
public class ZkCuratorRecipesConfig {
@Bean
public CuratorFramework getZkClient() throws InterruptedException {
String zkAddr = "192.168.17.128:2181";
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient(zkAddr, retryPolicy);
client.start();
return client;
}
}
@Autowired
CuratorFramework client;
@GetMapping("b")
public void b() throws Exception {
InterProcessMutex lock = new InterProcessMutex(client, "/lock");
if (lock.acquire(1, TimeUnit.SECONDS)) {
try {
logger.info("{} , {} ", Thread.currentThread().getName(), "获取到锁");
} finally {
lock.release();
}
}
}
稍微改造一下接口进行压测
@GetMapping("c")
public void c() throws Exception {
InterProcessMutex lock = new InterProcessMutex(client, "/lock");
String tName = Thread.currentThread().getName();
if (lock.acquire(1, TimeUnit.SECONDS)) {
try {
logger.info("{} , {} ", tName, "获取到锁");
Thread.sleep(500);
} finally {
lock.release();
logger.info("{} , {} ", tName, "释放锁");
}
}
}
效果图如下
原理分析
长时间没有收到心跳,zk会删除临时节点。
zk绝对安全吗
可以看到极端情况下,zk也有极小概率冲突。
数据库
以 MySQL为例
insert 和 delete
建一张分布式锁表,通过写入和删除数据来实现分布式锁。
悲观锁
利用for update语句。适用场景:写多读少。存在锁表风险。
乐观锁
利用 version 版本号。适用场景:读多写少。
选型和总结
侧重性能和简单使用Redis。
侧重安全性使用zookeeper。
记住极端情况互斥性都会被打破,不要认为绝对可靠。
数据库来做分布式锁会有性能瓶颈。
高并发下分布式锁优化
优化思想:锁分段、读写锁、无锁。