目录
环境准备
一 . Redis 安装
二:Spring boot 项目准备
三:nginx 安装
四:Jmeter 下载和配置
案例实战
优化一:加 synchronized 锁
优化二:使用 redis 的 setnx 实现分布式锁
优化三:使用 Lua 脚本 原子删除锁
优化四:使用 Redission 实现分布式锁
环境准备
一 . Redis 安装
1. Redis 下载: https://github.com/tporadowski/redis/releases
2. 解压
3.进入到目录下的cmd,执行如下命令启动 redis
redis-server.exe redis.windows.conf
二:Spring boot 项目准备
1. 引入 Maven 依赖
<?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.2.1.RELEASE</version>
</parent>
<groupId>com.xinxin</groupId>
<artifactId>cyh</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.5</version>
</dependency>
</dependencies>
</project>
2. 修改 application.properties 配置文件
spring.application.name=cyh spring.redis.host=localhost spring.redis.port=6379
3. 提供 web 服务 Controller
@RestController
public class RedissionController {
@Autowired
private RedissonClient redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/add_stock")
public String addStock() {
stringRedisTemplate.opsForValue().set("good_stock", "60");
return "ok";
}
@RequestMapping("/sub_stock")
public String deductStock() {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
return "ok";
}
}
4. 分别启动端口为8085和8086 端口的 Spring boot服务
三:nginx 安装
使用 nginx 来进行负载均衡,轮询调用 端口为8085和8086的服务,进行扣减库存。
项目架构图如下:
1. nginx 下载地址:https://nginx.org/en/download.html
2. 解压
3. 在conf 文件中修改 nginx.conf 配置文件
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
upstream redislock{
server 127.0.0.1:8085 weight=1;
server 127.0.0.1:8086 weight=1;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
proxy_pass http://redislock;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
主要配置了 8085 和 8086 服务的负载均衡
4. 启动nginx 服务,点击以下命令
nginx.exe
在任务管理中,看到nginx就代表启动成功了
四:Jmeter 下载和配置
1. jmeter 下载地址:Apache JMeter - Download Apache JMeter
2.解压
3. 进入 bin 目录,运行 jmeter.bat 文件(jmeter运行环境需要配置JDK环境)
4. 配置 jmeter
1. 新增线程组
设置线程数和循环次数
2. 新增 http 请求
配置nginx的域名端口 和 Spring boot项目的 请求路径
3.新增查看结果树和聚合报告
至此 压测分布式环境搭建完成。
案例实战
初始化 Redis 库存,在浏览器执行下面链接
http://localhost:8085/add_stock
启动 jmeter 进行压测
执行结果如下
从 8085 和 8086 服务的执行日志来看,8085服务中不仅出现重复扣减的问题 ,而且与8086服务中也存在重复扣减库存问题。
优化一:加 synchronized 锁
针对上面的问题,我们通常会加 synchronized 锁,来解决并发问题,修改代码如下
@RequestMapping("/sub_stock1")
public String deductStock1() {
synchronized (this) {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}
return "ok";
}
重启 8085 和 8086 服务,启动 jmeter 再次压测,结果如下
从 8085 和 8086 服务的执行日志来看,同一个服务中不会出现并发问题,但不同服务,比如8085 和8086服务就会出现重复扣减库存的问题。
加 synchronized 锁缺点:在分布式的场景下,还是会出现分布式问题
优化二:使用 redis 的 setnx 实现分布式锁
@RequestMapping("/sub_stock2")
public String deductStock2() {
String lockKey = "lock_good_stock";
String lockValue = UUID.randomUUID().toString();
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (!isLock) {
return "error";
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "ok";
}
注意: 设置值和设置过期时间不能分开写,不然也会出现服务器宕机或者启动,导致锁无法释放的问题 Boolean isLock =stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue); stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
上面代码还存在什么问题呢?
其实,上面的代码在高并发场景下,还是会出现问题,问题是锁过期,当锁过期时间到了之后,则会出现两个问题.
- 下一个线程B会获取到锁,执行扣减库存,导致并发问题
-
上一个线程A执行完时,又把锁释放了,导致下一个线程C又可以获取到锁。
针对锁失效导致的问题,对于第一个问题可以把锁的过期时间调长一点,针对第二个问题,可以先判断是不是自己加的锁,只有自己加的锁才删除。修改代码,如下:
@RequestMapping("/sub_stock3")
public String deductStock3() {
String lockKey = "lock_good_stock";
String lockValue = UUID.randomUUID().toString();
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 5, TimeUnit.MINUTES);
if (!isLock) {
return "error";
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
if (lockValue.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "ok";
}
上面代码看似没问题,但删除锁的代码还是存在问题。判断和删除是两行代码,存在原子性问题。等系统并发变高,系统执行速度变慢,锁可能还是会失效,而判断和删除不是原子性,所以线程A 还是会将线程B 的锁给删除了。在下个优化解决。
优化三:使用 Lua 脚本 原子删除锁
@RequestMapping("/sub_stock4")
public String deductStock4() {
String lockKey = "lock_good_stock";
String lockValue = UUID.randomUUID().toString();
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 5, TimeUnit.MINUTES);
if (!isLock) {
return "error";
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
final String script =
"local lockValue = redis.call('get', KEYS[1])" +
"if lockValue == ARGV[1] then " +
" redis.call('del', KEYS[1])" +
"end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script);
stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue);
}
return "ok";
}
至此一把完善的分布式锁就搞定了。这对于很多中小型公司来说已经够用,但在用户量比较多,并发比较高的公司来锁,可能还是会存在一定问题。比如锁失效的问题,此时可以使用业务比较流行的框架 Redission 来解决。
优化四:使用 Redission 实现分布式锁
@RequestMapping("/sub_stock5")
public String deductStock5() {
String lockKey = "lock_good_stock";
RLock lock = redisson.getLock(lockKey);
lock.lock(60, TimeUnit.SECONDS);
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
lock.unlock();
}
return "ok";
}
redission 解决了我们上面所有锁提高的问题,包括分布式,原子性和锁失效问题。redission 中有个看门口的机制,当业务还没执行的时候,会不断地给锁续命,是锁不会失效。