手写 Redis 分布式锁
面试题
- Redis 除了拿来做缓存,你还见过基于 Redis 的什么用法?
- 数据共享,分布式 Session
- 分布式锁
- 全局 ID
- 计算器,点赞
- 位统计
- 购物车
- 轻量级消息队列
- list
- stream
- 抽奖
- 点赞、签到、打卡
- 差集交集并集,用户关注、可能认识的人,推荐模型
- 热点新闻、搜索排行榜
- Redis 做分布式锁的时候需要注意的问题?
- 你们公司自己实现的分布式锁是否用的 setnx 命令实现?这个是最合适的吗?你如何考虑分布式锁的可重入问题?
- 如果 Redis 是单点部署的,会带来什么问题?那你准备怎么解决单点问题?
- Redis 集群模式下,比如主从模式,CAP 方面有没有问题呢?
- 那你简单的介绍一下 Redlock 吧?你简历上写 Redisson,你谈谈?
- Redis 分布式锁如何续期,看门狗知道吗?
锁的种类
-
单机版同一个 JVM 虚拟机内,synchronized 或者 Lock 接口;
-
分布式多个不通 JVM 虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。
分布式锁的条件和刚需
- 独占性:任何时刻只能有且仅有一个线程持有
- 高可用
- 若 Redis 集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
- 高并发请求下,依旧性能好使
- 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
- 不乱抢:防止张冠李戴,不能私下解别人的锁,自己加锁只能自己释放
- 重入性:同一个节点的同一个线程如果获得锁之后,他也可以再次获取这个锁
分布式锁
set key value [EX seconds] [PX milliseconds] [NX|XX]
- EX:Key 在多少秒之后过期
- PX:Key 在多少毫秒之后过期
- NX:当 Key 不存在的时候,才创建 Key,效果等同于 setnx
- XX:当 Key 存在的时候,覆盖 Key
127.0.0.1:6379> set lock pay ex 10 NX
OK
127.0.0.1:6379> get lock
"pay"
127.0.0.1:6379> set lock order ex 10 NX
OK
127.0.0.1:6379> set lock pay ex 10 NX
(nil)
127.0.0.1:6379> get lock
(nil)
127.0.0.1:6379>
如果 setnx
和 expire
两个命令分开执行,由于非原子性,可能存在潜在风险
重点
JUC 中 AQS 锁的规范落地参考 + 可重入锁考虑 + Lua 脚本 + Redis 命令一步步实现分布式锁
基础案例(Spring Boot + Redis)
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.matrixlab.redislock</groupId>
<artifactId>redis_distributed_lock2</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.12</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<lombok.version>1.16.18</lombok.version>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringBoot与Redis整合依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--通用基础配置boottest/lombok/hutool-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
server.port=7777
spring.application.name=redis_distributed_lock
# ========================swagger2=====================
# http://localhost:7777/swagger-ui.html
swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================redis单机=====================
spring.redis.database=0
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
package dev.matrixlab.redislock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedisDistributedLockApp7777 {
public static void main(String[] args) {
SpringApplication.run(RedisDistributedLockApp7777.class,args);
}
}
package dev.matrixlab.redislock.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Value("${swagger2.enabled}")
private Boolean enabled;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(enabled)
.select()
.apis(RequestHandlerSelectors.basePackage("dev.matrixlab.redislock")) //你自己的package
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
.description("springboot+redis整合")
.version("1.0")
.termsOfServiceUrl("https://www.baidu.com/")
.build();
}
}
package dev.matrixlab.redislock.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
lock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
lock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
package dev.matrixlab.redislock.controller;
import cn.hutool.core.util.IdUtil;
import dev.matrixlab.redislock.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣减库存,一次卖一个")
@GetMapping(value = "/inventory/sale")
public String sale() {
return inventoryService.sale();
}
}
手写分布式锁
0x01. 初始化版本简单添加
代码示例
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
lock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
lock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
注意点
增加了synchronized 或者 Lock
0x02. nginx 分布式微服务架构
问题
分布式需求
当 V2.0 版本代码分布式部署后,单机锁还是出现超卖现象,需要分布式锁
nginx 配置负载均衡
启动两个微服务
模拟高并发
出现超卖现象
Bug-Why
为什么加了 synchronized 或者 Lock 还是没有控制住?
在单机环境下,可以使用 synchronized 或 Lock 来实现。
但在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个 JVM 中),所以需要一个让所有进程都能访问到的锁来实现(比如 Redis 或者 Zookeeper 来构建)。
不同进程 JVM 层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁。未获取到锁,则阻塞当前想要运行的线程。
分布式锁作用
- 跨进程 + 跨服务
- 解决超卖
- 防止缓存击穿
解决方法
- 使用 Redis 分布式锁
setnx
- 官网 https://redis.io/commands/set (opens in a new tab)
0x03. Redis 分布式锁
3.1 版
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
String key = "RedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
if(!flag) {
//暂停20毫秒后递归调用
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
sale();
} else {
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
stringRedisTemplate.delete(key);
}
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
存在的 Bug
- 上述使用递归重试的方法,容易导致
StackOverflowError
3.2 版
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
String key = "RedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {
//暂停20毫秒,类似CAS自旋
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
} try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
stringRedisTemplate.delete(key);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
改进
- 多线程判断思想
JUC
里面说过的虚假唤醒,用while
替代if
- 使用自旋替代递归重试
存在的 Bug
- 部署了微服务的 Java 程序机器宕机,代码层面根本没有走到
finally
这块,没有办法保证解锁(无过期时间,该 Key 一直存在),需要加入一个过期时间限定 Key。
0x04. 宕机与过期 + 防止死锁
4.1 版
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
String key = "RedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
stringRedisTemplate.expire(key, 10L, TimeUnit.SECONDS);
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
stringRedisTemplate.delete(key);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
存在的 Bug
- 设置 Key 和过期时间分开,存在潜在风险,必须要合并成一行,具备原子性。
4.2 版
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
String key = "RedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
stringRedisTemplate.delete(key);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
改进
- 加锁和过期时间设置必须同一行,保证原子性。
存在的 Bug
- 实际业务处理时间超过了默认设置 Key 的过期时间
- 删除别人的锁
0x05. 防止误删 key 的问题
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
String key = "RedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
// v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
stringRedisTemplate.delete(key);
}
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
改进
- 不可随意删除别人创建的锁
存在的Bug
finally
代码块的判断和delete
删除操作不是原子性的
0x06. Lua 保证原子性
Lua 脚本编写 Redis 分布式锁
https://redis.io/docs/reference/patterns/distributed-locks/ (opens in a new tab)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Lua 脚本浅谈
Redis 调用 Lua 脚本通过 eval
命令保证代码执行的原子性,直接用 return
返回脚本执行后的结果值。
Lua 脚本初识
-
eval luascript numkeys [key [key ...]] [arg [arg ...]]
-
helloword 入门
-
hello lua
127.0.0.1:6379> EVAL "return 'hello lua'" 0 "hello lua" 127.0.0.1:6379>
-
set k1 v1 get k1
127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> get k1 "v1" 127.0.0.1:6379> EVAL "redis.call('set', 'k1', 'v1') return redis.call('get', 'k1')" 0 "v1" 127.0.0.1:6379> get k1 "v1" 127.0.0.1:6379>
-
mset
127.0.0.1:6379> EVAL "return redis.call('mset', KEYS[1], ARGV[1], KEYS[2], ARGV[2])" 2 k1 k2 11 12 OK 127.0.0.1:6379> get k1 "11" 127.0.0.1:6379> get k2 "12" 127.0.0.1:6379>
-
Lua 脚本进一步
-
Redis 分布式 Lua 脚本官网练习
127.0.0.1:6379> set RedisLock 1111-2222-3333 OK 127.0.0.1:6379> get RedisLock "1111-2222-3333" 127.0.0.1:6379> eval "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 1111-2222-3333 (integer) 1 127.0.0.1:6379> del RedisLock (integer) 0 127.0.0.1:6379> eval "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 1111-2222-3333 (integer) 0 127.0.0.1:6379>
-
条件判断语法
if (布尔条件) then 业务代码 elseif (布尔条件) then 业务代码 elseif (布尔条件) then 业务代码 else 业务代码 end
-
提交判断案例
127.0.0.1:6379> eval "if KEYS[1] > KEYS[2] then return ARGV[1] else return ARGV[2] end" 2 7 5 1 0 "1" 127.0.0.1:6379> eval "if KEYS[1] > KEYS[2] then return ARGV[1] else return ARGV[2] end" 2 7 8 1 0 "0" 127.0.0.1:6379>
程序实现
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
String key = "RedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)) {
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
//V6.0 将判断+删除自己的合并为lua脚本保证原子性
String luaScript =
"if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
0x07. 可重入锁 + 设计模式
问题
如何兼顾锁的可重入性问题?
可重入锁(有名递归锁)
概述
可重入锁是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java 中 ReentrantLock
和 synchronized
都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁”解释
- 可:可以
- 重:再次
- 入:进入
- 锁:同步锁
- 进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。即自己可以获取自己的内部锁。
JUC 知识复习,可重入锁出现 Bug 会如何影响程序
- 死锁:如果可重入锁的实现有 Bug,导致锁的释放出现问题,可能会发生死锁。当一个线程在持有锁的情况下再次尝试获取该锁时,如果由于 Bug 导致无法正确释放锁资源,其他线程将无法获取到锁,从而导致死锁的发生。
- 死循环:如果可重入锁的实现有 Bug,导致某个线程无法正确释放锁,那么当其他线程尝试获取同一个锁时,它们可能会陷入等待状态,从而导致死循环。这会消耗大量的 CPU 资源,并且可能导致整个程序的性能下降。
- 程序逻辑错误:可重入锁的 Bug 可能导致锁的获取和释放顺序出现问题,从而破坏了程序的预期逻辑。例如,如果在某个线程持有锁期间又尝试获取同一个锁,但由于 Bug 导致锁的状态无法正确更新,可能会导致程序逻辑上的错误或数据一致性问题。
- 并发安全性问题:可重入锁的 Bug 可能导致多个线程在并发访问共享资源时出现问题。例如,如果某个线程持有锁并修改了共享资源,但由于 Bug 导致其他线程无法正确获取锁,那么可能会导致数据竞争和并发安全性问题。
可重入锁种类
-
隐式锁(即
synchronized
关键字使用的锁)默认是可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个
synchronized
修饰的方法或代码块的内部调用本类的其他synchronized
修饰的方法或代码块时,是永远可以得到锁的。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。-
同步块
ReEntryLockDemo.javapackage dev.matrixlab.juc.senior.prepare; public class ReEntryLockDemo { public static void main(String[] args) { final Object objectLockA = new Object(); new Thread(() -> { synchronized (objectLockA) { System.out.println("-----外层调用"); synchronized (objectLockA) { System.out.println("-----中层调用"); synchronized (objectLockA) { System.out.println("-----内层调用"); } } } }, "a").start(); } }
-
同步方法
ReEntryLockDemo.javapackage dev.matrixlab.juc.senior.prepare; /** * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的 */ public class ReEntryLockDemo { public synchronized void m1() { System.out.println("-----m1"); m2(); } public synchronized void m2() { System.out.println("-----m2"); m3(); } public synchronized void m3() { System.out.println("-----m3"); } public static void main(String[] args) { ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo(); reEntryLockDemo.m1(); } }
-
-
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。synchronized
的重入的实现机理当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加 1,否则需要等待,直至持有线程释放该锁。
当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减 1。计数器为零代表锁已被释放。
-
显式锁(即
Lock
)也有ReentrantLock
这样的可重入锁ReEntryLockDemo.javapackage dev.matrixlab.juc.senior.prepare; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的 */ public class ReEntryLockDemo { static Lock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println("----外层调用lock"); lock.lock(); try { System.out.println("----内层调用lock"); } finally { // 这里故意注释,实现加锁次数和释放次数不一样 // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。 lock.unlock(); // 正常情况,加锁几次就要解锁几次 } } finally { lock.unlock(); } }, "a").start(); new Thread(() -> { lock.lock(); try { System.out.println("b thread----外层调用lock"); } finally { lock.unlock(); } }, "b").start(); } }
lock/unlock 配合可重入锁进行 AQS 源码分析
切记,一般而言,你 lock 了几次就要 unlock 几次。
Redis 数据类型替代可重入锁计数问题
-
HSET
-
Map<String, Map<Object, Object>>
-
案例命令
127.0.0.1:6379> HEXISTS RedisLock 1111-2222 (integer) 0 127.0.0.1:6379> HSET RedisLock 1111-2222 1 (integer) 1 127.0.0.1:6379> HINCRBY RedisLock 1111-2222 1 (integer) 2 127.0.0.1:6379> HINCRBY RedisLock 1111-2222 1 (integer) 3 127.0.0.1:6379> HGET RedisLock 1111-2222 "3" 127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1 (integer) 2 127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1 (integer) 1 127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1 (integer) 0 127.0.0.1:6379> HGET RedisLock 1111-2222 "0" 127.0.0.1:6379>
hset key field value # 锁名字 请求线程的uuid + threadID 加锁次数
-
总结
setnx
只能解决又无的问题,够用但是不完美hset
不但解决有无,还解决可重入问题
设计思路
目前有 2 条支线,目的是保证同一个时候只能有一个线程持有锁进去 Redis 做扣减库存动作
-
保证加锁/解锁 lock/unlock
一个靠谱分布式锁需要具备的条件和刚需
- 独占性
- 高可用
- 防死锁
- 不乱抢
- 重入性
-
扣减库存 redis 命令的原子性
Lua 脚本
redis 命令过程分析
127.0.0.1:6379> HEXISTS RedisLock 1111-2222
(integer) 0
127.0.0.1:6379> HSET RedisLock 1111-2222 1
(integer) 1
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 1
(integer) 2
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 1
(integer) 3
127.0.0.1:6379> HGET RedisLock 1111-2222
"3"
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
(integer) 2
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
(integer) 1
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
(integer) 0
127.0.0.1:6379> HGET RedisLock 1111-2222
"0"
127.0.0.1:6379> DEL RedisLock
(integer) 1
127.0.0.1:6379>
加锁 Lua 脚本 lock
-
先判断 Redis 分布式锁这个 Key 是否存在
EXISTS key
-
返回 0 说明不存在,HSET 新建当前线程属于自己的锁
UUID:ThreadID
127.0.0.1:6379> HSET RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 1 (integer) 1
-
返回 1 说明已经有锁,需进一步判断是不是当前线程自己的锁
# HEXISTS key UUID:ThreadID 127.0.0.1:6379> HEXISTS RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 (integer) 1
-
返回 0 说明不是自己的
-
返回 1 说明是自己的锁,自增 1 次表示重入
127.0.0.1:6379> HEXISTS 0c90d37cb6ec42268861b3d739f8b3a8:1 (integer) 1 127.0.0.1:6379> HINCRBY 0c90d37cb6ec42268861b3d739f8b3a8:1 1 (integer) 2 127.0.0.1:6379> HINCRBY 0c90d37cb6ec42268861b3d739f8b3a8:1 1 (integer) 3 127.0.0.1:6379>
-
-
-
上述设计修改为 Lua 脚本
-
V1
相同部分是否可以替换处理?hincrby 命令可否替代 hset 命令。
if redis.call('exists','key') == 0 then redis.call('hset','key','uuid:threadid',1) redis.call('expire','key',30) return 1 elseif redis.call('hexists','key','uuid:threadid') == 1 then redis.call('hincrby','key','uuid:threadid',1) redis.call('expire','key',30) return 1 else return 0 end
-
V2
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 then redis.call('hincrby','key','uuid:threadid',1) redis.call('expire','key',30) return 1 else return 0 end
-
V3
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end
K K V key KEYS[1] RedisLock value ARGV[1] 0c90d37cb6ec42268861b3d739f8b3a8:1 过期时间值 ARGV[2] 30 秒
-
-
测试
127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> HGET RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 "1" 127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> HGET RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 "3" 127.0.0.1:6379>
加锁 Lua 脚本 unlock
-
设计思路:有锁且还是自己的锁
# HEXISTS key uuid:ThreadID 127.0.0.1:6379> HEXISTS RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 (integer) 0
-
返回 0,说明根本没有锁,程序块返回
nil
-
不是 0,说明有锁且是自己的锁,直接调用
HINCRBY -1
表示每次减个 1,解锁 1 次,知道它变为 0 表示可以删除该锁 Key,DEL KEY
。
127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> HEXISTS RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 (integer) 1 127.0.0.1:6379> HGET RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 "3" 127.0.0.1:6379> HINCRBY RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 -1 (integer) 2 127.0.0.1:6379> HINCRBY RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 -1 (integer) 1 127.0.0.1:6379> HINCRBY RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 -1 (integer) 0 127.0.0.1:6379> DEL RedisLock (integer) 1 127.0.0.1:6379>
-
-
上述设计修改为 Lua 脚本
-
V1
if redis.call('HEXISTS',lock,uuid:threadID) == 0 then return nil elseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 then return redis.call('del',lock) else return 0 end
-
V2
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('DEL',KEYS[1]) else return 0 end
-
-
全套流程测试
127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 (integer) 1 127.0.0.1:6379> EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 (integer) 0 127.0.0.1:6379> EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 (integer) 0 127.0.0.1:6379> EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 (integer) 1 127.0.0.1:6379> EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 (nil) 127.0.0.1:6379>
程序实现
-
复原程序为初始无锁版
InventoryService.javapackage dev.matrixlab.redislock.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @Service @Slf4j public class InventoryService { @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; public String sale() { String retMessage = ""; //1 查询库存信息 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2 判断库存是否足够 Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); //3 扣减库存 if (inventoryNumber > 0) { stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"; System.out.println(retMessage); } else { retMessage = "商品卖完了,o(╥﹏╥)o"; } return retMessage+"\t"+"服务端口号:"+port; } }
-
新建 RedisDistributedLock 类并实现 JUC 里面的 Lock 接口
-
满足 JUC 里面的 AQS 对 Lock 锁的接口规范定义来进行实现落地代码
-
结合设计模式开发属于自己的 Redis 分布式锁工具类
-
Lua 脚本
-
加锁 lock
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end
-
解锁 unlock
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end
-
-
工厂设计模式引入
-
通过实现 JUC 里面的 Lock 接口,实现 Redis 分布式锁
RedisDistributedLock
RedisDistributedLock.javapackage dev.matrixlab.redislock.mylock; import cn.hutool.core.util.IdUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.support.collections.DefaultRedisList; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; //@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到 public class RedisDistributedLock implements Lock { private StringRedisTemplate stringRedisTemplate; private String lockName; //KEYS[1] private String uuidValue; //ARGV[1] private long expireTime; //ARGV[2] public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) { this.stringRedisTemplate = stringRedisTemplate; this.lockName = lockName; this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId(); //UUID:ThreadID this.expireTime = 30L; } @Override public void lock() { tryLock(); } @Override public boolean tryLock() { try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();} return false; } /** * 实现加锁功能,实现这一个干活的就OK,全盘通用 * @param time * @param unit * @return * @throws InterruptedException */ @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { if (time != -1L) { this.expireTime = unit.toSeconds(time); } String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " + "redis.call('hincrby',KEYS[1],ARGV[1],1) " + "redis.call('expire',KEYS[1],ARGV[2]) " + "return 1 " + "else " + "return 0 " + "end"; System.out.println("script: "+script); System.out.println("lockName: "+lockName); System.out.println("uuidValue: "+uuidValue); System.out.println("expireTime: "+expireTime); while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) { TimeUnit.MILLISECONDS.sleep(50); } return true; } /** *实现解锁功能 */ @Override public void unlock() { String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " + " return nil " + "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " + " return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; // nil = false 1 = true 0 = false System.out.println("lockName: "+lockName); System.out.println("uuidValue: "+uuidValue); System.out.println("expireTime: "+expireTime); Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime)); if(flag == null) { throw new RuntimeException("This lock doesn't EXIST"); } } //===下面的redis分布式锁暂时用不到======================================= @Override public void lockInterruptibly() throws InterruptedException { } @Override public Condition newCondition() { return null; } }
-
inventoryService
直接使用上面的代码设计,可能出现哪些问题?Lock redisDistributedLock = new RedisDistributedLock(stringRedisTemplate, "RedisLock"); ... redisDistributedLock.lock();
这样 redis 获得锁被写死了,假如以后有 Zookeeper,MySQL 做分布式锁,需要引入工厂模式。
-
引入工厂模式改造 7.1 版
DistributedLockFactory.javapackage dev.matrixlab.redislock.mylock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.locks.Lock; @Component public class DistributedLockFactory { @Autowired private StringRedisTemplate stringRedisTemplate; private String lockName; public Lock getDistributedLock(String lockType) { if (lockType == null) return null; if (lockType.equalsIgnoreCase("REDIS")) { lockName = "RedisLock"; return new RedisDistributedLock(stringRedisTemplate,lockName); } else if (lockType.equalsIgnoreCase("ZOOKEEPER")) { //TODO zookeeper版本的分布式锁实现 return new ZookeeperDistributedLock(); } else if (lockType.equalsIgnoreCase("MYSQL")) { //TODO mysql版本的分布式锁实现 return null; } return null; } }
RedisDistributedLock.javapackage dev.matrixlab.redislock.mylock; import cn.hutool.core.util.IdUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.support.collections.DefaultRedisList; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; //@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到 public class RedisDistributedLock implements Lock { private StringRedisTemplate stringRedisTemplate; private String lockName;//KEYS[1] private String uuidValue;//ARGV[1] private long expireTime;//ARGV[2] public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) { this.stringRedisTemplate = stringRedisTemplate; this.lockName = lockName; this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID this.expireTime = 30L; } @Override public void lock() { tryLock(); } @Override public boolean tryLock() { try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();} return false; } /** * 实现加锁功能,实现这一个干活的就OK,全盘通用 * @param time * @param unit * @return * @throws InterruptedException */ @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { if (time != -1L) { this.expireTime = unit.toSeconds(time); } String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " + "redis.call('hincrby',KEYS[1],ARGV[1],1) " + "redis.call('expire',KEYS[1],ARGV[2]) " + "return 1 " + "else " + "return 0 " + "end"; System.out.println("script: "+script); System.out.println("lockName: "+lockName); System.out.println("uuidValue: "+uuidValue); System.out.println("expireTime: "+expireTime); while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) { TimeUnit.MILLISECONDS.sleep(50); } return true; } /** * 实现解锁功能 */ @Override public void unlock() { String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " + " return nil " + "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " + " return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; // nil = false 1 = true 0 = false System.out.println("lockName: "+lockName); System.out.println("uuidValue: "+uuidValue); System.out.println("expireTime: "+expireTime); Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime)); if(flag == null) { throw new RuntimeException("This lock doesn't EXIST"); } } //===下面的redis分布式锁暂时用不到======================================= @Override public void lockInterruptibly() throws InterruptedException { } @Override public Condition newCondition() { return null; } }
InventoryService.javapackage dev.matrixlab.redislock.service; import ch.qos.logback.core.joran.conditional.ThenAction; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import dev.matrixlab.redislock.mylock.DistributedLockFactory; import dev.matrixlab.redislock.mylock.RedisDistributedLock; import lombok.extern.slf4j.Slf4j; import org.omg.IOP.TAG_RMI_CUSTOM_MAX_STREAM_FORMAT; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Service @Slf4j public class InventoryService { @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; @Autowired private DistributedLockFactory distributedLockFactory; public String sale() { String retMessage = ""; Lock redisLock = distributedLockFactory.getDistributedLock("redis"); redisLock.lock(); try { //1 查询库存信息 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2 判断库存是否足够 Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); //3 扣减库存 if (inventoryNumber > 0) { inventoryNumber = inventoryNumber - 1; stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t服务端口:" +port; System.out.println(retMessage); return retMessage; } retMessage = "商品卖完了,o(╥﹏╥)o"+"\t服务端口:" +port; } catch (Exception e) { e.printStackTrace(); } finally { redisLock.unlock(); } return retMessage; } }
-
单机 + 并发测试
-
-
可重入性测试
可重入测试
InventoryService 类新增可重入测试方法package dev.matrixlab.redislock.service;
import dev.matrixlab.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.locks.Lock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale() {
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
System.out.println(retMessage);
testReEnter();
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} catch (Exception e){
e.printStackTrace();
} finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter() {
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
System.out.println("################测试可重入锁#######");
} finally {
redisLock.unlock();
}
}
}
/**
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
*/
出现 ThreadID 一致了,但是 UUID 不一致。
引入工厂模式改造 7.2 版
package com.atguigu.redislock.mylock;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
@Component
public class DistributedLockFactory {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
public DistributedLockFactory() {
this.uuidValue = IdUtil.simpleUUID();//UUID
}
public Lock getDistributedLock(String lockType) {
if (lockType == null) return null;
if (lockType.equalsIgnoreCase("REDIS")) {
lockName = "RedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue);
} else if(lockType.equalsIgnoreCase("ZOOKEEPER")) {
//TODO zookeeper版本的分布式锁实现
return new ZookeeperDistributedLock();
} else if(lockType.equalsIgnoreCase("MYSQL")) {
//TODO mysql版本的分布式锁实现
return null;
}
return null;
}
}
package dev.matrixlab.redislock.mylock;
import cn.hutool.core.util.IdUtil;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class RedisDistributedLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
private long expireTime;
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock() {
this.tryLock();
}
@Override
public boolean tryLock() {
try {
return this.tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if(time != -1L) {
expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
}
return true;
}
@Override
public void unlock() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if(flag == null) {
throw new RuntimeException("没有这个锁,HEXISTS查询无");
}
}
//=========================================================
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import dev.matrixlab.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
* InventoryService 类新增可重入测试方法
*/
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale() {
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
this.testReEnter();
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} catch (Exception e) {
e.printStackTrace();
} finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter() {
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
System.out.println("################测试可重入锁####################################");
} finally {
redisLock.unlock();
}
}
}
单机 + 并发 + 可重入性测试
0x08. 自动续期
问题
确保 redisLock 过期时间大于业务执行时间的问题,Redis 分布式锁如何续期?
CAP
-
Redis 集群是 AP
redis 异步复制造成的锁丢失。比如,主节点没来得及把刚刚
set
进来这条数据给从节点,master 就挂了,从机上位但从机上无该数据。 -
Zookeeper 集群式 CP
-
CP
zk 是 CP 原理。假如 1 号机注册给 server1,server1 同步给 server2,server2 同步给各个 follower,为保证一致性,只有整个过程都成功了,1 号机才收到注册成功。
-
故障
当 leader 重启或者网络故障下,整个 zk 集群会重新选举 master,选举期间 client 不可以注册,即 zk 不可用,所以牺牲了可用性 A。只有选举出新老大后,系统才恢复注册。故 zk 为零保证数据一致性牺牲了可靠性。由于在大型分布式系统中故障难以避免,leader 出故障可能性很高,所以很多大型系统都不会选择 zk。
-
-
Euraka 集群式 AP
Eureka 是 AP 原理,保证高可用性而牺牲了一致性。client1 注册给 server1,server1 直接告诉 client 成功,client1 高兴的走了,剩下的就是 server1 再同步给 server2,为保证高可用,都是异步同步的。
-
Nacos 集群式 AP
服务注册与发现框架 CAP 模型 控制台管理 社区活跃度 Eureka AP 支持 低(2.x 版本闭源) Zookeeper CP 不支持 中 Consul CP 支持 高 Nacos AP 支持 高
Lua 脚本实现自动续期
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
新增自动续期功能
package dev.matrixlab.redislock.mylock;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class RedisDistributedLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate,String lockName,String uuidValue) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if(time != -1L) {
this.expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("script: "+script);
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(50);
}
this.renewExpire();
return true;
}
/**
* 实现解锁功能
*/
@Override
public void unlock() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
if(flag == null) {
throw new RuntimeException("This lock doesn't EXIST");
}
}
private void renewExpire() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
renewExpire();
}
}
},(this.expireTime * 1000)/3);
}
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
package dev.matrixlab.redislock.service;
import cn.hutool.core.util.IdUtil;
import dev.matrixlab.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale() {
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
//暂停几秒钟线程,为了测试自动续期
try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }
} else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} catch (Exception e) {
e.printStackTrace();
} finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter() {
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
System.out.println("################测试可重入锁####################################");
} finally {
redisLock.unlock();
}
}
}
InventoryService
- 记得去掉可重入测试
testReEnter()
InventoryService
业务逻辑里面故意sleep
一段时间测试自动续期
总结
synchronized
单机版可以,但是在分布式系统中无法使用- nginx 分布式微服务单机锁不行
- 取消单机锁,使用 redis 分布式锁
setnx
- 只加锁,没有释放锁;如果出现异常,可能无法释放锁;必须在代码层面
finally
释放锁 - 如果发生宕机,部署了微服务代码层面根本没有走到
finally
代码块,没办法保证解锁,这个 key 没有被删除,需要有lockKey
的过期时间设定 - 为 redis 的分布式锁 key 增加过期时间,并且必须保证
setnx
和expire
的两条命令的原子性操作- 必须规定自己的锁只能自己删除,无法删除别人的锁
unlock
变为 Lua 脚本保证- 锁重入,
hset
代替setnx
和lock
变为 Lua 脚本保证 - 锁的自动续期
- 只加锁,没有释放锁;如果出现异常,可能无法释放锁;必须在代码层面