Redis應用(二) --分散式鎖以及壓測介紹
Spring-boot 整合Redis應用(二) --分散式鎖以及壓測介紹
一.基礎環境
jdk 1.8
maven 3.5.3
spring-boot 2.0.4
redis 4.0.11
-
ApacheBench 2.3
以上工具需要提前安裝以及熟悉基本操作,在本文中不會講解如何安裝
二.基本介紹
相信各位小夥伴在學習Redis時,都瞭解到Redis不僅僅是一個記憶體中的資料結構儲存系統,它可以用作資料庫、快取和訊息中介軟體。上一篇整合應用已經簡單的介紹了 Redis作為訊息佇列配合基於Servlet 3的非同步請求處理的簡單示例。本篇將介紹Redis在單機部署的場景下的分散式鎖。分散式鎖的思想來源於Redis官網,下面給出中文翻譯相當棒連結,方便大家瞭解分散式鎖。《Redis官方文件》用Redis構建分散式鎖。在這就不講解中心思想了。那麼我會以一個模擬秒殺系統的簡單Demo來一步一步的展示redis分散式鎖的應用。
三.無布式鎖時的秒殺程式碼以及壓測結果
元件依賴
由於是無redis鎖的情況下的秒殺demo,則只需要引入spring-boot基礎依賴即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
程式碼
由於是一個簡單的秒殺下單的demo程式碼,那麼只需要兩個介面,一個是下單介面(order),一個是查詢訂單介面(query),Controller層程式碼如下:
/**
* @author Neal
* 測試無分散式鎖controller 層
*/
@RestController
@RequestMapping("/order")
public class NoDistributeController {
//無分散式鎖service
@Autowired
NoDistributeService redisDistributeService;
/**
* 查詢剩餘訂單結果介面
* @param pid 訂單編號
* @return
*/
@GetMapping("/query/{pid}")
public String query(@PathVariable String pid) {
return redisDistributeService.queryMap(pid);
}
/**
* 下單介面
* @param pid 訂單編號
* @return
*/
@GetMapping("/{pid}")
public String order(@PathVariable String pid) {
redisDistributeService.order(pid);
return redisDistributeService.queryMap(pid);
}
}
service層程式碼如下:
/**
* @author Neal
* 測試無分散式鎖service 層
*/
@Service
public class NoDistributeService {
//模擬商品資訊表
private static Map<String,Integer> products;
//模擬庫存表
private static Map<String,Integer> stock;
//模擬訂單表
private static Map<String,String> orders;
static {
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
//模擬訂單表資料 訂單編號 112233 庫存 100000
products.put("112233",100000);
//模擬庫存表資料 訂單編號112233 庫存100000
stock.put("112233",100000);
}
/**
* 模擬查詢秒殺成功返回的資訊
* @param pid 商品編號
* @return 返回拼接的秒殺商品結果字串
*/
public String queryMap(String pid) {
return "秒殺商品限量:" + products.get(pid) + "份,還剩:"+stock.get(pid) +"份,成功下單:"+orders.size() + "人";
}
/**
* 下單方法
* @param pid 商品編號
*/
public void order(String pid) {
//從庫存表中獲取庫存餘量
int stockNum = stock.get(pid);
//如果庫存為0 則輸出庫存不足
if(stockNum == 0) {
System.out.println("商品庫存不足");
}else{ //如果有庫存
//往訂單表中插入資料 生成UUID作為使用者ID pid
orders.put(UUID.randomUUID().toString(),pid);
//執行緒休眠 模擬其他操作
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//減庫存操作
stock.put(pid,stockNum-1);
}
}
}
程式碼上有註釋,我就不過多的囉嗦解釋了,那麼讓我們來模擬秒殺環境,使用 ApacheBench 來模擬併發,來看看結果如何:
進入到 ApacheBench 的bin目錄下,我的路徑是 Apache24\bin。
-
輸入命令以及引數:ab -n 1000 -c 100 http://127.0.0.1:8080/order/112233 該命令的含義是 向指定的URL傳送 1000次請求 100 併發量。 結果如圖
雖然只耗時了2.596秒,但是處理的結果卻不盡如人意。
- 然後使用查詢介面查詢一下下單和庫存結果是否一致,請求查詢介面:http://localhost:8080/order/query/112233。結果如圖
可以看出下單人數和庫存餘量明顯不符,就這就是無鎖時,在高併發環境中會引起的問題。
Redis分散式鎖下的秒殺程式碼以及壓測結果
前言
網上看了很多的例子,都是使用redis的SETNX命令來實現的,Redis 官網並不推薦使用SETNX命令,而是推薦使用SET,因為從2.6.12版本以後,Redis對SET命令增加了一系列的選項。
EX
seconds – Set the specified expire time, in seconds.PX
milliseconds – Set the specified expire time, in milliseconds.NX
– Only set the key if it does not already exist.XX
– Only set the key if it already exist.EX
seconds – 設定鍵key的過期時間,單位時秒PX
milliseconds – 設定鍵key的過期時間,單位時毫秒NX
– 只有鍵key不存在的時候才會設定key的值-
XX
– 只有鍵key存在的時候才會設定key的值注意: 由於
SET
命令加上選項已經可以完全取代SETNX, SETEX, PSETEX的功能,所以在將來的版本中,redis可能會不推薦使用並且最終拋棄這幾個命令。 原文地址
所以本例也是使用上述文章所推薦的加鎖解鎖方法。
加鎖:使用命令 SET resource_name my_random_value NX PX 30000 這個命令的作用是在只有這個key不存在的時候才會設定這個key的值(NX選項的作用),超時時間設為30000毫秒(PX選項的作用) 這個key的值設為“my_random_value”。這個值必須在所有獲取鎖請求的客戶端裡保持唯一。
解鎖: 使用LUA指令碼語言
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
這段指令碼的意思是:刪除這個key當且僅當這個key存在而且值是我期望的那個值。
LUA指令碼的原子性 原文連結
Redis 使用單個 Lua 直譯器去執行所有指令碼,並且, Redis 也保證指令碼會以原子性(atomic)的方式執行:當某個指令碼正在執行的時候,不會有其他指令碼或 Redis 命令被執行。這和使用 MULTI / EXEC 包圍的事務很類似。在其他別的客戶端看來,指令碼的效果(effect)要麼是不可見的(not visible),要麼就是已完成的(already completed)。
另一方面,這也意味著,執行一個執行緩慢的指令碼並不是一個好主意。寫一個跑得很快很順溜的指令碼並不難,因為指令碼的執行開銷(overhead)非常少,但是當你不得不使用一些跑得比較慢的指令碼時,請小心,因為當這些蝸牛指令碼在慢吞吞地執行的時候,其他客戶端會因為伺服器正忙而無法執行命令。
元件依賴
相關jedis依賴
<!--Jedis 相關依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
程式碼
1.配置jedis。網上有很多配置jedis的例子,我就給一個簡單的配置實現。我使用的是自定義的配置,首先在 application.properties中新增Jedis配置引數
#jedis相關配置
#Redis IP
jedis.host=192.168.56.101
#Redis 埠
jedis.port=6379
#Redis 密碼
jedis.password=123456
jedis.timeout=3
jedis.poolMaxTotal=10
jedis.poolMaxIdle=10
jedis.poolMaxWait=3
2.宣告自定義配置Bean,使用springboot 註解ConfigurationProperties來載入application.properties中的配置引數。
/**
* @author Neal
* 自定義Jedis配置bean
*/
@Component
@ConfigurationProperties(prefix = "jedis")
public class MyJedisBean {
private String host;
private int port;
private String password;
private int timeout;
private int poolMaxTotal;
private int poolMaxIdle;
private int poolMaxWait;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getPoolMaxTotal() {
return poolMaxTotal;
}
public void setPoolMaxTotal(int poolMaxTotal) {
this.poolMaxTotal = poolMaxTotal;
}
public int getPoolMaxIdle() {
return poolMaxIdle;
}
public void setPoolMaxIdle(int poolMaxIdle) {
this.poolMaxIdle = poolMaxIdle;
}
public int getPoolMaxWait() {
return poolMaxWait;
}
public void setPoolMaxWait(int poolMaxWait) {
this.poolMaxWait = poolMaxWait;
}
}
3.生成JedisPool元件bean
這裡就是把JedisPool的相關配置引數配置到JedisPoolConfig並且利用JedisPool的構造方法來宣告物件。
/**
* @author Neal
* 初始化jedis 連線池
*/
@Component
public class MyJedisConfig {
/**
* 自定義jedis配置bean
*/
@Autowired
private MyJedisBean myJedisBean;
@Bean
public JedisPool jedisPoolFactory() {
//宣告jedispool 配置類
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(myJedisBean.getPoolMaxIdle());
jedisPoolConfig.setMaxTotal(myJedisBean.getPoolMaxTotal());
jedisPoolConfig.setMaxWaitMillis(myJedisBean.getPoolMaxWait() *1000);
/**
* 利用Jedis的構造方法 生成 jedispool
*/
JedisPool jedisPool = new JedisPool(jedisPoolConfig,myJedisBean.getHost(),myJedisBean.getPort(),myJedisBean.getTimeout()*1000,myJedisBean.getPassword(),0);
return jedisPool;
}
}
4.實現Redis分散式鎖方法
該類中只有加鎖(redisLock)和解鎖(redisUnlock)兩個方法。 宣告的靜態變數 都是根據Redis原生命令 SET resource_name my_random_value NX PX 30000 宣告的命令字串。在加鎖和解鎖時 resource_name 對應的是 商品ID, my_random_value 對應的是我們用UUID 生成的模擬使用者ID。
/**
* @author Neal
* 分散式鎖
*/
@Component
public class MyRedisLock {
//Only set the key if it does not already exist.
private static final String IF_NOT_EXIST = "NX";
// Set the specified expire time, in milliseconds.
private static final String SET_EXPIRE_TIME = "PX";
//超時時間為 500毫秒
private static final int EXPIRE_TIME = 500;
//加鎖成功後返回的標識
private static final String ON_LOCK = "OK";
//LUA 解鎖指令碼
private static final String LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* 獲取配置好的jedis pool 元件
*/
@Autowired
private JedisPool jedisPoolFactory;
/***
* redis 加鎖方法
* @param id 商品ID
* @param uuid 模擬使用者ID
* @return 返回 true 加鎖成功 false 解鎖成功
*/
public boolean redisLock(String id,String uuid) {
//從 jedis 連線池中 獲取jedis
Jedis jedis = jedisPoolFactory.getResource();
boolean locked = false;
try{
//使用Jedis 加鎖
locked = ON_LOCK.equals(jedis.set(id,uuid,IF_NOT_EXIST,SET_EXPIRE_TIME,EXPIRE_TIME));
}finally {
//將連線放回連線池
jedis.close();
}
return locked;
}
/***
* redis 解鎖方法
* @param id 商品ID
* @param uuid 模擬使用者ID
* @return 由於是使用LUA指令碼,則會保證原子性的特質
*/
public void redisUnlock(String id,String uuid) {
//從 jedis 連線池中 獲取jedis
Jedis jedis = jedisPoolFactory.getResource();
try{
//使用Jedis 的 eval解鎖
Object result = jedis.eval(LUA_SCRIPT, Collections.singletonList(id),Collections.singletonList(uuid));
if(1L == (Long)result) {
System.out.println("客戶ID為:《" + uuid + "》 解鎖成功!");
}
}finally {
jedis.close();
}
}
}
核心的加鎖程式碼已經介紹完了,下面就是關於 在秒殺service層加鎖與解鎖相關的程式碼了。
5.Controller層
controller層與之前的無鎖controller沒有變化,還是一樣的程式碼。
/**
* @author Neal
* 測試分散式鎖controller 層
*/
@RestController
@RequestMapping("/distribute")
public class RedisDistributeController {
@Autowired
private RedisDistributeService redisDistributeService;
@Autowired
private JedisPool jedisPoolFactory;
@GetMapping("/query/{pid}")
public String query(@PathVariable String pid) {
return redisDistributeService.queryMap(pid);
}
@GetMapping("/{pid}")
public String order(@PathVariable String pid) {
redisDistributeService.order(pid, UUID.randomUUID().toString());
return redisDistributeService.queryMap(pid);
}
}
6.service層
在service層中的下單方法(order)中加入了 加鎖與解鎖的操作。
/**
* @author Neal
* 測試分散式鎖service 層
*/
@Service
public class RedisDistributeService {
//模擬商品資訊表
private static Map<String,Integer> products;
//模擬庫存表
private static Map<String,Integer> stock;
//模擬訂單表
private static Map<String,String> orders;
//redis 鎖元件
@Autowired
MyRedisLock myRedisLock;
static {
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("112233",100000);
stock.put("112233",100000);
}
/**
* 模擬查詢秒殺成功返回的資訊
* @param pid 商品名稱
* @return
*/
public String queryMap(String pid) {
return "秒殺商品限量:" + products.get(pid) + "份,還剩:"+stock.get(pid) +"份,成功下單:"+orders.size() + "人";
}
/**
* 下單方法
* @param pid 商品名稱
*/
public void order(String pid,String uuid) {
//redis 加鎖
if(!myRedisLock.redisLock(pid,uuid)) { //如果沒獲得鎖則直接返回,不執行下面的程式碼
System.out.println("客戶ID為:《"+ uuid +"》未獲得鎖");
return;
}
System.out.println("客戶ID為:《"+ uuid +"》獲得鎖");
//從庫存表中獲取庫存餘量
int stockNum = stock.get(pid);
if(stockNum == 0) {
System.out.println("商品庫存不足");
}else{
//往訂單表中插入資料
orders.put(uuid,pid);
//執行緒休眠 模擬其他操作
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//減庫存操作
stock.put(pid,stockNum-1);
}
//redis 解鎖
myRedisLock.redisUnlock(pid,uuid);
}
}
<u>程式碼已經介紹完了那麼下面開始壓測,並看看結果是否跟沒加鎖的程式碼有區別</u>
7.壓測
壓測方式跟上面講述的一樣,我就不在囉嗦了,直接上結果圖。
由圖中可以看出,同樣的壓測命令,秒殺的結果卻只有13個秒殺成功,但是庫存餘量與秒殺的數量是對應的上的,不會出現庫存與秒殺數量不一致問題。
結論:
在寫Redis鎖的時候看了很多前輩的博文以及教學視訊,發現之前用的都是基於SETNX,解鎖也是各有千秋,最後還是參考了 Redis的官方實踐。
DEMO程式碼地址
Redis命令大全
相關文章
- Redis 應用-分散式鎖Redis分散式
- Redis Redisson 分散式鎖的應用和原始碼Redis分散式原始碼
- 月半談(一)redis-分散式鎖與應用Redis分散式
- 分散式鎖簡單入門以及三種實現方式介紹分散式
- Redis分散式鎖的原理以及如何續期Redis分散式
- 十九、Redis分散式鎖、Zookeeper分散式鎖Redis分散式
- 分散式鎖-Redis分散式Redis
- Redis分散式鎖Redis分散式
- Redis 分散式鎖Redis分散式
- 用 Go + Redis 實現分散式鎖GoRedis分散式
- Redis HyperLogLog介紹及應用Redis
- Redis分散式鎖解析Redis分散式
- redis系列:分散式鎖Redis分散式
- Springboot + redis分散式鎖Spring BootRedis分散式
- Redis 分散式鎖(一)Redis分散式
- 分散式鎖(5)-MLock使用介紹(自己實現,基於redis,適用於真實專案)分散式Redis
- Redis分散式鎖這樣用,有坑?Redis分散式
- Redis分散式鎖加鎖案例Redis分散式
- redis分散式鎖-可重入鎖Redis分散式
- 【Redis的那些事 · 上篇】Redis的介紹、五種資料結構演示和分散式鎖Redis資料結構分散式
- seata分散式事務AT模式介紹(二)分散式模式
- JAVA 分散式 - 分散式介紹Java分散式
- 循序漸進 Redis 分散式鎖(以及何時不用它)Redis分散式
- redis實現分散式鎖(包含程式碼以及分析利弊)Redis分散式
- redis應用系列一:分散式鎖正確實現姿勢Redis分散式
- 簡單介紹redis分散式鎖解決表單重複提交的問題Redis分散式
- 分散式鎖----Redis實現分散式Redis
- 細說Redis分散式鎖?Redis分散式
- Redis分散式鎖實戰Redis分散式
- Redis實現分散式鎖Redis分散式
- 【180414】分散式鎖(redis/mysql)分散式RedisMySql
- 基於 Redis 分散式鎖Redis分散式
- 詳解Redis分散式鎖Redis分散式
- 【Redis】利用 Redis 實現分散式鎖Redis分散式
- Redis元件介紹(二)Redis元件
- jmeter 分散式壓測,以及 使用的一些技巧JMeter分散式
- ZooKeeper分散式專題(一) -- zookeeper安裝以及介紹分散式
- WEB 應用快取解析以及使用 Redis 實現分散式快取Web快取Redis分散式