版本1
案例
@RestController
public class IndexController {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/deductStock")
public String deductStock() throws InterruptedException{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减库存成功,剩余库存:"+realStock);
}else{
System.out.println("扣减库存失败,库存不足。");
}
return "end";
}
}
jmeter压力测试:
启动50个线程测试,结果输出如下:
2021-01-03 20:10:30.616 INFO 4326 --- [io-8080-exec-29] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-01-03 20:10:30.616 INFO 4326 --- [io-8080-exec-29] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-01-03 20:10:30.620 INFO 4326 --- [io-8080-exec-29] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
2021-01-03 20:10:30.718 INFO 4326 --- [io-8080-exec-15] io.lettuce.core.EpollProvider : Starting without optional epoll library
2021-01-03 20:10:30.719 INFO 4326 --- [io-8080-exec-15] io.lettuce.core.KqueueProvider : Starting without optional kqueue library
扣减库存成功,剩余库存:48
扣减库存成功,剩余库存:48
扣减库存成功,剩余库存:48
扣减库存成功,剩余库存:48
......
扣减库存成功,剩余库存:48
扣减库存成功,剩余库存:48
可以看出 结果不正确,如何解决?synchronized加锁;
版本2
加锁方式可以实现线程安全,正常扣减库存;
@GetMapping("/deductStock")
public String deductStock() throws InterruptedException{
synchronized (this) {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减库存成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足。");
}
}
return "end";
}
但是单线程影响性能,更重要的是synchronized是基于JVM进程的;分布式系统通常要集群部署的。
2021-01-03 20:14:50.853 INFO 4354 --- [io-8080-exec-42] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-01-03 20:14:50.858 INFO 4354 --- [io-8080-exec-42]
.....
扣减库存成功,剩余库存:47
扣减库存成功,剩余库存:46
.....
扣减库存成功,剩余库存:3
扣减库存成功,剩余库存:2
扣减库存成功,剩余库存:1
版本3
集群方式部署微服务,先以8080方式启动服务,再以8081方式启动服务;开启Nginx负载均衡。
@GetMapping("/deductStock")
public String deductStock() throws InterruptedException{
synchronized (this) {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减库存成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足。");
}
}
return "end";
}
Nginx配置反向代理:
#gzip on;
#redislock demo
upstream redislock{
server localhost:8080 weight=1;
server localhost:8081 weight=1;
}
server {
listen 8090;
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;
}
.......
-
启动8080微服务
-
配置IDEA再以8081端口启动微服务
(1) 点击修改配置菜单
(2) 再弹出的窗口执行如下操作
(3)设置启动的主类和端口号
(4) 通过nginx访问tomcat微服务集群
8080微服务:
8081微服务:
3. 通过jmeter压力测试 200 个线程测试
执行结果如下图所示:
有大量的重复数据,显然不符合业务需求。
版本4
使用redis分布式锁实现;
package com.kongfanyu.controller;
import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Copyright (C), 2018-2021
* FileName: IndexController
* Author: kongfanyu
* Date: 2021/1/3 下午5:07
*/
@RestController
public class IndexController {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//版本3.0 使用redis分布式锁
@GetMapping("/deductStock")
public String deductStock() throws InterruptedException {
String lockKey = "product_001";
String clientId = UUID.randomUUID().toString();
try {
//Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "fuzi");
//stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
Boolean present = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (!present){
return "error";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减库存成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足。");
}
}finally {
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
//释放锁
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
/*
//版本v2.0 使用同步锁机制--不能解决集群情况下的数据一致性
@GetMapping("/deductStock")
public String deductStock() throws InterruptedException{
synchronized (this) {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减库存成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足。");
}
}
return "end";
}*/
/*
//版本v1.0 扣减数量不对
@GetMapping("/deductStock")
public String deductStock() throws InterruptedException{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0){
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减库存成功,剩余库存:"+realStock);
}else{
System.out.println("扣减库存失败,库存不足。");
}
return "end";
}
*/
}
重新启动两个微服务,再次测试集群状态下的数据:
8080微服务
2021-01-03 21:43:31.778 INFO 4933 --- [nio-8080-exec-3] io.lettuce.core.KqueueProvider : Starting without optional kqueue library
扣减库存成功,剩余库存:48
扣减库存成功,剩余库存:47
扣减库存成功,剩余库存:45
扣减库存成功,剩余库存:44
扣减库存成功,剩余库存:43
扣减库存成功,剩余库存:41
扣减库存成功,剩余库存:38
扣减库存成功,剩余库存:37
扣减库存成功,剩余库存:34
扣减库存成功,剩余库存:33
扣减库存成功,剩余库存:32
8081微服务
扣减库存成功,剩余库存:49
扣减库存成功,剩余库存:46
扣减库存成功,剩余库存:42
扣减库存成功,剩余库存:40
扣减库存成功,剩余库存:39
扣减库存成功,剩余库存:36
扣减库存成功,剩余库存:35
扣减库存成功,剩余库存:31
扣减库存成功,剩余库存:30
扣减库存成功,剩余库存:29
可以看出系统正常执行。
版本5
生成环境使用redission分布式锁: redisson.org
//版本4.0 使用redisson分布式锁
@GetMapping("/deductStock")
public String deductStock() throws InterruptedException {
String lockKey = "product_001";
String clientId = UUID.randomUUID().toString();
RLock redissonLock = redisson.getLock(lockKey);
try {
redissonLock.lock(30, TimeUnit.SECONDS);
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减库存成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足。");
}
}finally {
redissonLock.unlock();//释放锁
}
return "end";
}
再次通过Nginx访问Tomcat集群:
8080微服务执行结果:
扣减库存成功,剩余库存:49
扣减库存成功,剩余库存:46
扣减库存成功,剩余库存:45
扣减库存成功,剩余库存:43
扣减库存成功,剩余库存:42
扣减库存成功,剩余库存:38
扣减库存成功,剩余库存:36
扣减库存成功,剩余库存:35
扣减库存成功,剩余库存:34
扣减库存成功,剩余库存:33
扣减库存成功,剩余库存:32
扣减库存成功,剩余库存:25
扣减库存成功,剩余库存:24
8081微服务执行结果:
扣减库存成功,剩余库存:48
扣减库存成功,剩余库存:47
扣减库存成功,剩余库存:44
扣减库存成功,剩余库存:41
扣减库存成功,剩余库存:40
扣减库存成功,剩余库存:39
扣减库存成功,剩余库存:37
扣减库存成功,剩余库存:31
扣减库存成功,剩余库存:30
扣减库存成功,剩余库存:29
扣减库存成功,剩余库存:28
可以看出正常执行。