在高并發(fā)場景下進(jìn)行減庫存氏身,該應(yīng)用程序是分布式部署,使用nginx做負(fù)載均衡梆造,使用redis做分布式鎖
?使用Spirngboot開發(fā)測試
- 1.首先搭建環(huán)境
pom文件
<?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>com.redis</groupId>
<artifactId>test-redis-lock</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.0</version>
</dependency>
</dependencies>
</project>
項(xiàng)目目錄
項(xiàng)目目錄
application.yaml文件
server:
port: 8080 #服務(wù)端口
spring:
redis:
host: 180.76.244.226 #redis服務(wù)器地址
啟動(dòng)器
@SpringBootApplication
public class TestRedisApplication {
public static void main(String[] args) {
SpringApplication.run(TestRedisApplication.class);
}
- 2.編寫減庫存代碼
package com.test.controller;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lipeng
* 時(shí)間: 2020-10-13 10:00
* 描述:
*/
@RestController
@RequestMapping("/api/demo")
public class Demo2Controller {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_lock")
public String deductLock() {
synchronized (this) {
//加鎖
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//根據(jù)key 獲取redis中的值
if (stock > 0) {
int realStock = stock - 1;
//將值再存入redis中
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("減庫存成功,剩余庫存數(shù):" + realStock);
} else {
System.out.println("當(dāng)前庫存數(shù)不足");
}
}
return "end";
}
}
-
3.分布式部署項(xiàng)目
由于使用的是Springboot疑务,所以分布式部署還是很方便的暗甥,我們只需改變端口號宝与,并重新運(yùn)行一遍main函數(shù)即可
分布式部署
nginx負(fù)載均衡配置
upstream redislock{
server 192.168.110.99:8080 weight=1;
server 192.168.110.99:8081 weight=1;
}
server{
listen 9000;
server_name localhsot;
location / {
root html;
index index.html index.html;
proxy_pass http://redislock;
}
}
- 4.使用壓力測試進(jìn)行jmter進(jìn)行測試
jmter
我這里配置了200的并發(fā)量
http請求
之后就會發(fā)起請求,我們發(fā)現(xiàn)出現(xiàn)很多超賣請求
焚廊。冶匹。。咆瘟。嚼隘。。袒餐。飞蛹。要給redis中存一個(gè)值,stock 我存的50灸眼,這里就不截圖了 - 5.使用reids實(shí)現(xiàn)分布式鎖卧檐,并總結(jié)遇到的坑
package com.test.controller;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lipeng
* 時(shí)間: 2020-10-12 8:59
* 描述:高并發(fā)下減庫存的實(shí)現(xiàn)
*/
@RestController
@RequestMapping("/api/demo")
public class DemoController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Redisson redisson;
@RequestMapping("/deduct_lock")
public String deductLock() {
String lockKey = "lockKey";
/* Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lipeng"); //使用redis實(shí)現(xiàn)1.0分布式鎖
stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS); //10s超時(shí)*/
/* String clientId= UUID.randomUUID().toString(); //創(chuàng)建標(biāo)識,判斷是否是自己還持有鎖
//底層為原子塊執(zhí)行
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (!result) {
return "error";
}*/
/**
* -------------------------------------redis實(shí)現(xiàn)1.0版本的分布式鎖 bug 總結(jié)--------------------------------*
* 1.當(dāng)?shù)谝粋€(gè)線程進(jìn)入時(shí)幢炸,拿到了鎖泄隔,但是在執(zhí)行過程中拋出了一個(gè)異常,這個(gè)時(shí)候鎖并沒有釋放 產(chǎn)生死鎖現(xiàn)象.
* ----解決方案:加上一個(gè) try{} finally{} 使程序不管是否拋出異常 鎖必須釋放
*2.在應(yīng)用執(zhí)行過程中宛徊,服務(wù)宕機(jī)了,此時(shí)該應(yīng)用已經(jīng)拿到鎖逻澳,但是并沒有釋放闸天,之后別的機(jī)器上過來的請求依然會直接返回,再次產(chǎn)生死鎖狀態(tài)
*----解決方案:給這個(gè)鎖的key加一個(gè)超時(shí)時(shí)間斜做,10s后如果該key存在苞氮,直接自動(dòng)銷毀 stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
* 3.在程序執(zhí)行到給key加超時(shí)時(shí)間時(shí) 程序宕機(jī)了,又會產(chǎn)生死鎖
*----解決方案:StringRedisTemplate提供了一個(gè)api:stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lipeng", 10, TimeUnit.SECONDS)瓤逼;
* 4.在高并發(fā)場景下笼吟,由于程序執(zhí)行時(shí)間超出失效時(shí)間導(dǎo)致鎖失效問題
*----解決方案:創(chuàng)建一個(gè)標(biāo)識,在釋放鎖的時(shí)候霸旗,判斷是不是自己設(shè)置的鎖贷帮,如果是就釋放
* 5.在高并發(fā)場景下,鎖的超時(shí)時(shí)間解決
*----解決方案:使用redisson來實(shí)現(xiàn)分布式鎖(在加鎖成功后诱告,在后臺開啟一個(gè)線程撵枢,實(shí)現(xiàn)定時(shí)任務(wù),每隔10秒檢查是否還持有鎖如果持有則延長鎖的時(shí)間)
*/
//獲取鎖
RLock redissonLock = redisson.getLock(lockKey);
try {
//加鎖
redissonLock.lock();
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//根據(jù)key 獲取redis中的值
if (stock > 0) {
int realStock = stock - 1;
//將值再存入redis中
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("減庫存成功,剩余庫存數(shù):" + realStock);
} else {
System.out.println("當(dāng)前庫存數(shù)不足");
}
} finally {
//釋放鎖
redissonLock.unlock();
/*if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){ //判斷是否是自己加的鎖精居,如果是自己加的鎖就釋放
stringRedisTemplate.delete(lockKey);
}*/
}
return "end";
}
}
而最終我們選擇使用redisson,該框架對redis進(jìn)行了再次封裝锄禽,它的使用場景也多為分布式。
redisson分布式鎖實(shí)現(xiàn)原理