手写Redis分布式锁
锁的分类
单机版同一个JVM虚拟机内,synchronized或者Lock接口
分布式多个不同JVM虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。
分布式锁需要具备的条件和刚需
- 独占性
OnlyOne,任何时刻只能有且仅有一个线程持有- 高可用
若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
高并发请求下,依l旧性能OK好使- 防死锁
杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案- 不乱抢
防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放,自己约的锁含着泪也要自己解- 重入性
同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
分布式锁
setnx key value
base案例(boot+redis)(version 1)
- 使用场景
多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
- 建Module
redis_distributed_lock2
redis_distributed_lock3
- pom
<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>
- yml
server:
port: 7777
# ========================swagger2=====================
# http://localhost:7777/swagger-ui.html
swagger2:
enable: true
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# ========================redis单机=====================
redis:
host: 192.168.183.139
port: 6379
password: 123456
- service
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
lock.lock();
// 业务逻辑代码(扣减库存)
try {
//1. 查看库存信息
String result = redisTemplate.opsForValue().get("inventory001");
//2. 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.valueOf(result);
//3. 扣减库存
if (inventoryNumber > 0) {
redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
}else {
retMessage = "商品卖完了";
}
}finally {
lock.unlock();
}
return retMessage + "\t" + "服务端口号: " + port;
}
}
- controller
@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController
{
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣减库存,一次卖一个")
@GetMapping(value = "/inventory/sale")
public String sale()
{
return inventoryService.sale();
}
}
加入nginx分布式微服务架构(version 2)
- nginx配置负载均衡
nginx配置文件
default.conflocation / { proxy_pass http://mynginx; }
nginx.conf
upstream mynginx { server 192.168.44.1:7777 weight=1; server 192.168.44.1:8888 weight=1; }
启动nginx(docker)
docker start nginx
- 启动两个微服务手点测试(默认轮询)
通过nginx访问
- 高并发模拟
76号商品被卖出2次,出现超卖故障现象
- 解释
在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),
所以需要一个让所有进程都能访问到的锁来实现比如redis来构建
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
- 分布式锁出现
跨进程+跨服务
解决超卖
防止缓存击穿
Redis分布式锁(version 3)
- service
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${server.port}")
private String port;
public String sale() {
String retMessage = "";
String key = "redisLock";
String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
while (!redisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {
//暂停20毫秒,类似cas自选
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
// 业务逻辑代码(扣减库存)
try {
//1. 查看库存信息
String result = redisTemplate.opsForValue().get("inventory001");
//2. 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.valueOf(result);
//3. 扣减库存
if (inventoryNumber > 0) {
redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
}else {
retMessage = "商品卖完了";
}
}finally {
redisTemplate.delete(key);
}
return retMessage + "\t" + "服务端口号: " + port;
}
}
宕机与过期+防止死锁(version 4)
- version 3问题
部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块,
没办法保证解锁(无过期时间该key一直存在),这个key没有被删除,需要加入一个过期时间限定key
- 解决
给key设置过期时间,注意设置key+过期时间必须合并一行具备原子性
redisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)
- 修改service增加过期时间
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${server.port}")
private String port;
public String sale() {
String retMessage = "";
String key = "redisLock";
String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
while (!redisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
//暂停20毫秒,类似cas自选
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
// 业务逻辑代码(扣减库存)
try {
//1. 查看库存信息
String result = redisTemplate.opsForValue().get("inventory001");
//2. 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.valueOf(result);
//3. 扣减库存
if (inventoryNumber > 0) {
redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
}else {
retMessage = "商品卖完了";
}
}finally {
redisTemplate.delete(key);
}
return retMessage + "\t" + "服务端口号: " + port;
}
}
防止误删key的问题(version 5)
- version 4 的问题
实际业务处理时间如果超过了默认设置key的过期时间
张冠李戴,删除了别人的锁
- 解决
删除锁的时候先判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
redisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)
- 修改service增加判断条件
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${server.port}")
private String port;
public String sale() {
String retMessage = "";
String key = "redisLock";
String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
while (!redisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
//暂停20毫秒,类似cas自选
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
// 业务逻辑代码(扣减库存)
try {
//1. 查看库存信息
String result = redisTemplate.opsForValue().get("inventory001");
//2. 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.valueOf(result);
//3. 扣减库存
if (inventoryNumber > 0) {
redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
}else {
retMessage = "商品卖完了";
}
}finally {
// 判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
if (redisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)) {
redisTemplate.delete(key);
}
}
return retMessage + "\t" + "服务端口号: " + port;
}
}
Lua保证原子性(version 6)
- 问题
finally块的判断+del删除操作不是原子性的
- 解决
启用lua脚本编写redis分布式锁判断+删除判断代码
- Lua脚本简介
- Lua脚本初识
Redis调用Lua脚本通过eval命令保证代码执行的原子性,直接用return返回脚本执行后的结果值
eval luascript numkeys [key [key ...]][arg [arg ..]]
- Lua脚本进一步
Redis分布式锁Lua脚本官网练习
eval "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 zzyyRedisLock 1111-2222-3333
条件判断语法
条件判断案例
- 修改service
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${server.port}")
private String port;
public String sale() {
String retMessage = "";
String key = "redisLock";
String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
while (!redisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
//暂停20毫秒,类似cas自选
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
// 业务逻辑代码(扣减库存)
try {
//1. 查看库存信息
String result = redisTemplate.opsForValue().get("inventory001");
//2. 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.valueOf(result);
//3. 扣减库存
if (inventoryNumber > 0) {
redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
}else {
retMessage = "商品卖完了";
}
}finally {
String luaScript =
"if (redis.call('get', KEYS[1]) == ARGV[1]) then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
}
return retMessage + "\t" + "服务端口号: " + port;
}
}
可重入锁+设计模式(version 7)
- 问题
while判断并自旋重试获取锁+setnx含自然过期时间+Lua脚本官网删除锁命令
如何兼顾锁的可重入性问题?
- redis哪个数据类型可以代替
hset key field value
hset redis锁名字(zzyyRedisLock) 某个请求线程的UUID+ThreadID 加锁的次数
setnx,只能解决有无的问题
hset,不但解决有无,还解决可重入问题
- lua脚本
redis命令过程分析
加锁lua脚本lock
- 先判断redis分布式锁这个key是否存在
exists key
返回0说明不存在,hset
新建当前线程属于自己的锁by uuid:threadId
返回壹说明已经有锁,需进一步判断是不是当前线程自己的
HEXISTS key uuid:ThreadlD
返回壹说明是自己的锁,自增1次表示重入
hincrby key field increment
返回零说明不是自己的
加锁Lua脚本lockif 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
key value 过期时间值 KEYS[1] ARGV[1] ARGV[2] zzyyRedisLock 2f586ae740a94736894ab9d51880ed9d:1 30 秒
解锁lua脚本unlock
- 设计思路
hexists key uuid:threadId
返回0,表示没有锁,程序返回nil
不是0,说明有锁且还是自己的锁,直接hincrby -1
表示每次减一,解锁一次,直到它变成0表示可以删除改key,del锁key
- lua脚本
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
- 测试
整合lua脚本到java程序中
- 新建RedisDistributedLock类并实现JUC里面的Lock接口
- 满足JUC里面AQS对Lock锁的接口规范定义来进行实现落地代码
- 通过实现JUC里面的Lock接口,实现Redis分布式锁RedisDistributedLock
public class RedisDistributedLock implements Lock {
private StringRedisTemplate redisTemplate;
private String lockName;// KEYS[1]
private String uuidValue;// ARGV[1]
private long expireTime;// ARGV[2]
public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
this.expireTime = 40L;
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {tryLock(-1L, TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 实现加锁功能,实现这一个就可以了,其他的加锁方法都调用这个
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time != -1) {
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 (!redisTemplate.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 = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(lockName), uuidValue);
if (flag == null) {
throw new RuntimeException("This lock doesn't EXIST");
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
- 修改service代码
public String sale() {
Lock lock = new RedisDistributedLock(redisTemplate, "redisLock");
String retMessage = "";
lock.lock();
// 业务逻辑代码(扣减库存)
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
//1. 查看库存信息
String result = redisTemplate.opsForValue().get("inventory001");
//2. 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.valueOf(result);
//3. 扣减库存
if (inventoryNumber > 0) {
redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
}else {
retMessage = "商品卖完了";
}
}finally {
lock.unlock();
}
return retMessage + "\t" + "服务端口号: " + port;
}
- 引入工厂模式
考虑扩展,本次是redis实现分布式锁,以后zookeeper、mysql实现那
DistributedLockFactory
@Component
public class DistributedLockFactory {
@Autowired
private StringRedisTemplate redisTemplate;
private String lockName;
private String uuidValue;
public static final String REDIS_LOCK = "redis";
public static final String ZOOKEEPER_LOCK = "zookeeper";
public static final String MYSQL_LOCK = "mysql";
public DistributedLockFactory() {
this.uuidValue = IdUtil.simpleUUID();
}
public Lock getDistributedLock(String lockType) {
if (lockType == null) return null;
if (lockType.equalsIgnoreCase(REDIS_LOCK)) {
lockName = "redisLock";
return new RedisDistributedLock(redisTemplate, lockName, uuidValue);
}else if (lockType.equalsIgnoreCase(ZOOKEEPER_LOCK)) {
//TODO zookeeper版本的分布式锁实现
}else if (lockType.equalsIgnoreCase(MYSQL_LOCK)) {
//TODO mysql版本的分布式锁实现
}
return null;
}
}
RedisDistributedLock(主要更改构造方法)
public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockName, String uuidValue) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue + ":" + Thread.currentThread().getId();
this.expireTime = 30L;
}
service
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory lockFactory;
public String sale() {
String retMessage = "";
Lock redisLock = lockFactory.getDistributedLock(DistributedLockFactory.REDIS_LOCK);
redisLock.lock();
// 业务逻辑代码(扣减库存)
try {
//1. 查看库存信息
String result = redisTemplate.opsForValue().get("inventory001");
//2. 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.valueOf(result);
//3. 扣减库存
if (inventoryNumber > 0) {
redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
}else {
retMessage = "商品卖完了";
}
}finally {
redisLock.unlock();
}
return retMessage + "\t" + "服务端口号: " + port;
}
}
自动续期(version 8)
确保redisLock过期时间大于业务执行时间的问题
- 加个种,lua脚本
hset redisLock 111122223333:11 3
EXPIRE redisLock 30
ttl redisLock
#========================================
eval "if rediscall('hexists',KEYS[1],ARGV[1]) == 1 then return redis.call('expire', KEYS[1],ARGV[2]) else return 0 end" 1 redisLock 111122223333:11 30
#===============================自动续期
if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
- 修改RedisDistributeLock
public class RedisDistributedLock implements Lock {
private StringRedisTemplate redisTemplate;
private String lockName;// KEYS[1]
private String uuidValue;// ARGV[1]
private long expireTime;// ARGV[2]
public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockName, String uuidValue) {
this.redisTemplate = redisTemplate;
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;
}
/**
* 实现加锁功能,实现这一个就可以了,其他的加锁方法都调用这个
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time != -1) {
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 (!redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(50);
}
this.renewExpire();
return true;
}
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 (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
renewExpire();
}
}
},(this.expireTime * 1000) / 3);
}
/**
* 实现解锁功能
*/
@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 = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(lockName), uuidValue);
if (flag == null) {
throw new RuntimeException("This lock doesn't EXIST");
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}