Redis实现分布式锁
一、锁的种类
常见的锁有两个种类:
- 单机版同一个JVM虚拟机内,synchronized或者Lock接口
- 分布式多个不同JVM虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。
二、分布式锁具备的特点
实现的分布式锁,需要具备一下特征:
| 特点 | 描述 |
| 独占性 | 任何时刻有且只有一个线程持有使用该锁 |
| 高可用 |
在redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况; 在高并发请求下,分布式锁依旧具有良好的性能; |
| 防死锁 | 不能出现死锁问题,必须有超时重试机制或者撤销操作,有个终止跳出的途径; |
| 不乱抢 | 多线程下,防止张冠李戴,只能解锁自己的锁,不能把别人的锁给释放了; |
| 重入性 | 同一节点的同一线程如果获得锁之后,该线程可以再次获取使用这个锁 |
三、Redis中分布式锁的实现
在常规的实现方式中Redis锁机制一般是由setnx命令实现,,语法如下:
SETNX key value:
将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。
返回值Integer reply, 特定值:
- 1 如果key被设置了
- 0 如果key没有被设置
例如:
# 设置k1的值为1 127.0.0.1:6379> SETNX k1 1 1 # 设置k1的过期时间为60s 127.0.0.1:6379> EXPIRE k1 60 1
但是上面的etnx和expire实现分布式锁的方式是不安全,两条命令非原子性的,并不能保证一致性,所以下面就是自研分布式锁来实现
四、基础案例代码
分布式锁实现功能:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击),创建两个模块:redis_distributed_lock2和redis_distributed_lock3,内容一样的,直接复制即可
4.1.创建模块:
创建springboot模块:

设置包名,选择项目模式和Java版本

设置模块名称

4.2.添加依赖
在pom.xml引入需要的依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.12</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.augus.redis</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.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>
4.3.添加配置文件
在application.properties中添加配置文件
server.port=7777 spring.application.name=redis_distributed_lock # ========================swagger2===================== # http://localhost:7777/swagger-ui.html spring.swagger2.enabled=true spring.mvc.pathmatch.matching-strategy=ant_path_matcher # ========================redis单机===================== spring.redis.database=0 spring.redis.host=192.168.42.132 spring.redis.port=6379 spring.redis.password=123456 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
4.4.在service包下创建 InventoryService
在InventoryService中内容如下:
package com.augus.redis.service; 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; import java.util.concurrent.locks.ReentrantLock; @Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; private Lock lock = new ReentrantLock(); public String sale(){ String message = ""; lock.lock(); try { //1.查询库存 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2.判断库存是否充足 Integer inventoryNumber = null; if(result==null){ inventoryNumber=0; }else { inventoryNumber=Integer.parseInt(result); } //3.减扣库存 if(inventoryNumber>0){ //将库存自减1,然后保存 stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber)); message = "卖出一件商品,库存剩余:"+inventoryNumber; System.out.println(message); }else { message = "商品已经售完"; } }finally { lock.unlock(); } return message+"\t"+"服务端口号:"+port; } }
4.5.在controller包下创建 InventoryController
在InventoryController中内容如下:
package com.augus.redis.controller; import com.augus.redis.service.InventoryService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Api(tags = "Redis分布式锁测试") public class InventoryController { @Autowired private InventoryService inventoryService; @ApiOperation("扣减库存,一次卖一个") @GetMapping(value = "/inventory/sale") public String sale(){ return inventoryService.sale(); } }
4.5.在Redis中创建创建数据模拟商品库存
创建键 inventory001值为100,模拟有100件商品

4.6.测试
保证Redis启动,同时启动redis_distributed_lock2模块,访问swagger:http://localhost:7777/swagger-ui.html,点击执行减扣库存的接口:

查看Redis中数据,发现数据已经扣减

下面开始通过不断地迭代来对自研的分布式锁进行完善
五、Nginx分布式微服务架构
5.1.架构图实现如下
两个订单服务实现负载均衡,都去调用库存模块,库存模块通过Redis缓存库存数据,同时另外通过一个Redis实现分布式锁

5.2.安装nginx,设置负载均衡
下面讲解一下如何在CentOS7系统下快速安装Nginx。
5.2.1.下载Nginx安装包
进入Nginx官网(https://nginx.org/),点击 download 链接

在 Stable version 中,下载最新版本

5.2.2.安装依赖包
在CentOS7命令行模式下,依次输入以下命令,安装所需的依赖包
yum install -y gcc-c++ yum install -y pcre pcre-devel yum install -y zlib zlib-devel yum install -y openssl openssl-devel
5.2.3.将Nginx安装包拷贝到CentOS7系统并解压
工具连接CentOS7系统后这里我们将Nginx安装包放到 /opt 目录下。进入 /home 目录,输入 tar -zxvf nginx-1.24.0.tar.gz 命令,对安装包进行解压缩

5.2.4.配置Nginx
进入/opt/nginx-1.24.0 目录然后解压Nginx文件夹
./configure \ --prefix=/usr/local/nginx \ --pid-path=/var/local/nginx/nginx.pid \ --lock-path=/var/local/nginx/nginx.lock \ --error-log-path=/var/local/nginx/error.log \ --http-log-path=/var/local/nginx/access.log \ --with-http_gzip_static_module \ --http-client-body-temp-path=/var/local/nginx/client \ --http-proxy-temp-path=/var/local/nginx/proxy \ --http-fastcgi-temp-path=/var/local/nginx/fastcgi \ --http-uwsgi-temp-path=/var/local/nginx/uwsgi \ --http-scgi-temp-path=/var/local/nginx/scgi
5.2.5.编译并安装
编译,输入命令:
make
安装,编译完成后,输入命令:
make install
5.2.6.启动Nginx
进入到Nginx目录:
cd /usr/local/nginx/sbin
输入启动命令:
./nginx
此时使用浏览器访问CentOS7服务器的IP地址 http://192.168.42.132/,可以看到Nginx的首页。这里注意需要开启防火墙允许访问

5.2.6.Nginx的常用命令
Nginx的强行停止命令: # ./nginx -s stop Nginx的优雅停止命令: # ./nginx -s quit // 停止,是等最后一次交互执行完再停止。 Nginx检查配置文件是否有错: # ./nginx -t Nginx的重新加载命令: # ./nginx -s reload 查看Nginx版本: # ./nginx -v 查看Nginx详细版本: # ./nginx -V
5.2.7.新增反向代理和负载均衡配置
在 /usr/local/nginx/conf 目录下修改配置文件nginx.conf文件,添加配置,如下(建议修改前先保存一份,防止修改出现问题,便于恢复):
# upstream 各服务器地址以及权重,权重越大代表访问率越大 upstream alie.com { server 192.168.1.3:7777 weight=1; server 192.168.1.3:8888 weight=1; } server { location / { # 反向代理,这里的地址与上面配置的upstream需一致,实现负载均衡 proxy_pass http://alie.com; proxy_redirect default; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; }
如下图所示:

5.2.8.指定配置文件启动nginx
进入到:/usr/local/nginx/sbin目录,执行如下命令:
./nginx -c /usr/local/nginx/conf/nginx.conf
如下图所示

5.2.9.测试
启动Redis和两个微服务模块,通过nginx访问,地址的nginx服务器的IP,如:http://192.168.42.132/inventory/sale,如下图7777和8888交替出现


5.2.模拟高并发情况下访问
通过jmeter模块高并发下订单,演示商品超卖的情况,这里Redis中商品数量回复设置为100,jmeter设置如下:
使用100个线程1s内访问该接口

http请求设置如下:

执行后,正常而言,应该是redis中商品数量应该为0,才对,可是查看redis发现并非如此

查看控制台7777和8888服务的执行情况,发现98和99商品被卖了两次

为什么会出现上面同一件商品买了两次的情况?加了synchronized或者Lock还是没有控制住?
- 在单机环境下,可以使用synchronized或Lock来实现。
- 但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
- 因为不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
5.3.分布式锁
分布式锁可以使用例如像Redis、zookeeper等中间件实现,需要满足一下几点:
- 跨进程和跨服务
- 防止缓存击穿
- 解决超卖
这里可以通过Redis的分布式锁解决,通过Redis中setnx命令来实现
六、Redis实现分布式锁
6.1.递归重试的方式实现
6.1.1.修改InventoryService方法中内容
这里通过实现Redis所来解决商品超卖的现象,将 redis_distributed_lock2和redis_distributed_lock3的service层,修改 InventoryService为如下内容:
package com.augus.redis.service; import cn.hutool.core.util.IdUtil; 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.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port;//3.1版本 public String sale(){ String message = ""; //创建的锁的名字,在Redis中作为键存在 String lockKey = "redisLock"; //通过UUID和线程ID组合成值 String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId(); //在Redis中创建键值对作为锁使用 Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuidValue); //flag为true表示拿到锁,为false表示没有抢到锁,就要重试 if(!flag){ //暂停20毫秒后递归调用,重试获取锁 try { TimeUnit.MILLISECONDS.sleep(20); sale(); }catch (InterruptedException e){ e.printStackTrace(); } }else { try { //1.查询库存 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2.判断库存是否充足 Integer inventoryNumber = null; if(result==null){ inventoryNumber=0; }else { inventoryNumber=Integer.parseInt(result); } //3.减扣库存 if(inventoryNumber>0){ //将库存自减1,然后保存 stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber)); message = "卖出一件商品,库存剩余:"+inventoryNumber; System.out.println(message); }else { message = "商品已经售完"; } }finally { //用完了就删除该key stringRedisTemplate.delete(lockKey); } } return message+"\t"+"服务端口号:"+port; } }
6.1.2.测试
启动Redis,将键inventory001的值设置为100,重启 redis_distributed_lock2和redis_distributed_lock3微服务模块,执行jmeter中测试脚本

查看控制台,也没有出现商品超卖,一件商品买两次的情况。

查看Redis中商品库存减扣为0

6.2.自旋的方式实现重试
6.2.1.修改service中InventoryService代码
在6.1中通过递归的方式实现重试获取锁的操作,但是递归机制本身存在一定的问题,极其容易出现StackOverflowError 的问题,所以使用while代替if,用自选替代递归重试。修改service中InventoryService代码如下:
package com.augus.redis.service; import cn.hutool.core.util.IdUtil; 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.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; public String sale(){ String message = ""; //创建的锁的名字,在Redis中作为键存在 String lockKey = "redisLock"; //通过UUID和线程ID组合成值 String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId(); //这里通过自旋的方式,替换掉之前递归的方式 while (!stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuidValue)){ //暂停20毫秒后递归调用,重试获取锁 try { TimeUnit.MILLISECONDS.sleep(20); }catch (InterruptedException e){ e.printStackTrace(); } } try { //1.查询库存 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2.判断库存是否充足 Integer inventoryNumber = null; if(result==null){ inventoryNumber=0; }else { inventoryNumber=Integer.parseInt(result); } //3.减扣库存 if(inventoryNumber>0){ //将库存自减1,然后保存 stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber)); message = "卖出一件商品,库存剩余:"+inventoryNumber; System.out.println(message); }else { message = "商品已经售完"; } }finally { //用完了就删除该key stringRedisTemplate.delete(lockKey); } return message+"\t"+"服务端口号:"+port; } }
6.2.2.测试
将Redis中模块库存的键inventory001值设置为100,重启 redis_distributed_lock2和redis_distributed_lock3微服务模块,执行jmeter中测试脚本

查看控制台,也没有出现商品超卖,一件商品买两次的情况。这个锁谁拿到了谁用

查看Redis中库存数量,也是为0

七、Redis宕机、锁过期和防止死锁
在第六章节中部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块,无法保证解锁(无过期时间该key一直存在),这个key没有被删除,所以需要加入一个过期时间限定key,
7.1.分布式锁设置过期时间
但是需要注意加锁和过期时间的设置必须具有原子性,同时完成,如下图:

代码如下:
package com.augus.redis.service; import cn.hutool.core.util.IdUtil; 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.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; //v3.2版本 public String sale(){ String message = ""; //创建的锁的名字,在Redis中作为键存在 String lockKey = "redisLock"; //通过UUID和线程ID组合成值 String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId(); //这里通过自旋的方式,替换掉之前递归的方式 while (!stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuidValue,30L,TimeUnit.SECONDS)){ //暂停20毫秒后递归调用,重试获取锁 try { TimeUnit.MILLISECONDS.sleep(20); }catch (InterruptedException e){ e.printStackTrace(); } } try { //1.查询库存 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2.判断库存是否充足 Integer inventoryNumber = null; if(result==null){ inventoryNumber=0; }else { inventoryNumber=Integer.parseInt(result); } //3.减扣库存 if(inventoryNumber>0){ //将库存自减1,然后保存 stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber)); message = "卖出一件商品,库存剩余:"+inventoryNumber; System.out.println(message); }else { message = "商品已经售完"; } }finally { //用完了就删除该key stringRedisTemplate.delete(lockKey); } return message+"\t"+"服务端口号:"+port; } }
7.2.测试
启动Redis,将键inventory001的值设置为100,重启 redis_distributed_lock2和redis_distributed_lock3微服务模块,执行jmeter中测试脚本,然后查看Redis中库存数量为0,未出现超卖现象

八、解决误删key的问题
在实际业务中,有可能存在张冠李戴,删除了别人的锁,如下图
这个肯定是需要避免的,要求只能删除自己的锁,修改两个模块service包下InventoryService如下:

代码如下:
package com.augus.redis.service; import cn.hutool.core.util.IdUtil; 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.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; public String sale(){ String message = ""; //创建的锁的名字,在Redis中作为键存在 String lockKey = "redisLock"; //通过UUID和线程ID组合成值 String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId(); //这里通过自旋的方式,替换掉之前递归的方式 while (!stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuidValue,30L,TimeUnit.SECONDS)){ //暂停20毫秒后递归调用,重试获取锁 try { TimeUnit.MILLISECONDS.sleep(20); }catch (InterruptedException e){ e.printStackTrace(); } } try { //1.查询库存 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2.判断库存是否充足 Integer inventoryNumber = null; if(result==null){ inventoryNumber=0; }else { inventoryNumber=Integer.parseInt(result); } //3.减扣库存 if(inventoryNumber>0){ //将库存自减1,然后保存 stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber)); message = "卖出一件商品,库存剩余:"+inventoryNumber; System.out.println(message); }else { message = "商品已经售完"; } }finally { // 判断解锁是否和加的锁是同一个客户端,自己只能删除自己的锁,不误删他人的 // equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。 if(stringRedisTemplate.opsForValue().get(lockKey).equalsIgnoreCase(uuidValue)){ stringRedisTemplate.delete(lockKey); } } return message+"\t"+"服务端口号:"+port; } }
启动Redis,将键inventory001的值设置为100,重启 redis_distributed_lock2和redis_distributed_lock3微服务模块,执行jmeter中测试脚本,然后查看Redis中库存数量为0,未出现超卖现象

九、Lua脚本保证原子性
在第八章节中,增加了误删key的防止,但是finally中判断和删除并不是原子性的,这样会告知各种问题

所以本章就是采用lua脚本编写Redis分布式锁将判断删除key的操作通过原子性来代替,修改如下:

代码如下:
package com.augus.redis.service; import cn.hutool.core.util.IdUtil; 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 javax.annotation.Resource; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; //v3.2版本 public String sale(){ String message = ""; //创建的锁的名字,在Redis中作为键存在 String lockKey = "redisLock"; //通过UUID和线程ID组合成值 String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId(); //这里通过自旋的方式,替换掉之前递归的方式 while (!stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuidValue,30L,TimeUnit.SECONDS)){ //暂停20毫秒后递归调用,重试获取锁 try { TimeUnit.MILLISECONDS.sleep(20); }catch (InterruptedException e){ e.printStackTrace(); } } try { //1.查询库存 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2.判断库存是否充足 Integer inventoryNumber = null; if(result==null){ inventoryNumber=0; }else { inventoryNumber=Integer.parseInt(result); } //3.减扣库存 if(inventoryNumber>0){ //将库存自减1,然后保存 stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber)); message = "卖出一件商品,库存剩余:"+inventoryNumber; System.out.println(message); }else { message = "商品已经售完"; } }finally { //将判断和删除key的操作合并为lua脚本保证原子性 String luaScript = "if (redis.call('get',KEYS[1]) == ARGV[1]) then " + "return redis.call('del',KEYS[1]) " + "else " + "return 0 " + "end"; //执行lua脚本, Arrays.asList(lockKey)是传入给KEYS[1], ARGV[1]传入的是uuidValue // Arrays.asList该方法是将数组转化成List集合的方法。 stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript,Boolean.class), Arrays.asList(lockKey),uuidValue); } return message+"\t"+"服务端口号:"+port; } }
启动Redis,将键inventory001的值设置为100,重启 redis_distributed_lock2和redis_distributed_lock3微服务模块,执行jmeter中测试脚本,未出现超卖现象

同时查看Redis中,商品数量变为0

十、可重入锁结合设计模式实现和自动续期
可重入锁又名递归锁:是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了,就很麻烦。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
在Redis中可以通过setnx解决有无的效果,够用但是不够完美,hset不但能解决有无锁机制的问题,还可以解决重入锁机制的问题,下面的实现思路:
- 引入工厂模式 DistributedLockFactory, 实现 Lock 接口,实现redis的可重入锁
- lock() 加锁的关键逻辑
- 加锁 (实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间)
- 自旋
- 续期
- unlock() 解锁关键逻辑
- 将 Key 键删除,只能自己删自己的锁,不能删除其他线程的锁。
- lock() 加锁的关键逻辑
- 实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加时间自动续期的脚本。
创建mylock包,在下面创建:RedisDistributedLock,内容如下:
package com.augus.redis.mylock; 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; 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; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { if (time == -1L) { 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))) { // 暂停 60ms Thread.sleep(60); } // 新建一个后台扫描程序,来监视key目前的ttl,是否到我们规定的 1/2 1/3 来实现续期 resetExpire(); return true; } return false; } private void resetExpire() { //判断该键值是否存在Redis中,如果为1就自动续期,0就是直接已经将key删除了。不需要续期 String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " + "return redis.call('expire',KEYS[1],ARGV[2]) " + "else " + "return 0 " + "end"; //java定时器,schedule可以设置多久执行一次 new Timer().schedule(new TimerTask() { @Override public void run() { if (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue)){ resetExpire(); } } }, (this.expireTime * 1000) / 3);//设置过期时间,单位是毫秒,所以*1000,也就是10s就需要判断一次是否需要续期 } //实现解锁 @Override public void unlock() { //如果 //Redis Hincrby 命令用于为哈希表中的字段值加上指定增量值。增量也可以为负数,相当于对指定字段进行减法操作。 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"); } } // 下面两个用不上 @Override public Condition newCondition() { return null; } @Override public void lockInterruptibly() throws InterruptedException { } }
创建mylock包,在下面创建DistributedLockFactory引入工厂模式,去实例化RedisDistributedLock类对象然后调用,内容如下:
package com.augus.redis.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 = "hiRedisLock"; return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue); } else if(lockType.equalsIgnoreCase("ZOOKEEPER")){ //zookeeper版本的分布式锁实现 //return new ZookeeperDistributedLock(); return null; } else if(lockType.equalsIgnoreCase("MYSQL")){ //mysql版本的分布式锁实现 return null; } return null; } }
修改service包中的 InventoryService,代码如下:
package com.augus.redis.service; import com.augus.redis.mylock.DistributedLockFactory; 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 public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; @Resource private DistributedLockFactory distributedLockFactory; //最终版本实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本 public String sale(){ String message = ""; //指定锁的类型 Lock redisLock = distributedLockFactory.getDistributedLock("redis"); //加锁 redisLock.lock(); try { //1.查询库存 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2.判断库存是否充足 Integer inventoryNumber = null; if(result==null){ inventoryNumber=0; }else { inventoryNumber=Integer.parseInt(result); } //3.减扣库存 if(inventoryNumber>0){ //将库存自减1,然后保存 stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber)); message = "卖出一件商品,库存剩余:"+inventoryNumber; System.out.println(message); // 演示自动续期的的功能 /*try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }*/ }else { message = "商品已经售完"; } }finally { //释放锁 redisLock.unlock(); } return message+"\t"+"服务端口号:"+port; } }
启动Redis,将键inventory001的值设置为100,重启 redis_distributed_lock2和redis_distributed_lock3微服务模块,执行jmeter中测试脚本,未出现超卖现象


浙公网安备 33010602011771号