
秒殺系統的架構設計
秒殺系統,是典型的短時大量突發訪問類問題,對這類問題,有三種優化性能的思路:
- 寫入記憶體而不是寫入硬碟
- 異步處理而不是同步處理
- 分布式處理
用上這三招,不論秒殺時負載多大,都能輕松應對, Redis正好能完美滿足上述三點,因此,用Redis就能輕松實作秒殺系統,

秒殺測驗代碼撰寫:
package com.xiao.springbootredisseckill.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
@RestController
public class TestController {
@Autowired
private RedisTemplate redisTemplate;
// 記錄實際賣出的商品數量
private AtomicInteger successNum = new AtomicInteger(0);
// 設定庫存
@GetMapping("/setStockNum")
public String setStockNum(@RequestParam Integer num){
// 初始化庫存數量
redisTemplate.opsForValue().set("product_stock",num);
//初始化實際賣出的商品數量0
successNum.set(0);
return "Ok";
}
// 秒殺核心代碼
@GetMapping("/grabBuy")
public String grabBuy() {
Integer sku = (Integer)redisTemplate.opsForValue().get("product_stock");
sku = sku - 1;
if (sku < 0) {
return "庫存不足";
}
redisTemplate.opsForValue().set("product_stock", sku);
//記錄實際賣出的商品數量
return "減少庫存成功,共減少" + successNum.incrementAndGet();
}
// 獲取庫存數量
@GetMapping("/getStockNum")
public Object getStockNum() {
Integer product_stock = Integer.parseInt((String) redisTemplate.opsForValue().get("product_stock"));
System.out.println(product_stock);
return product_stock;
}
@GetMapping(value = "/successNum")
public String successNum() {
return "顧客成功搶到的商品數量:" + successNum.get();
}
}
測驗:
庫存設為 100, 訪問 127.0.0.1:8080/grabBuy 進行搶購,為了讓測驗場景更像實際搶購實際場景,這里使用 Apache 其下的 JMeter 壓力測驗工具,具體如何使用請自行百度
訪問127.0.0.1:8080/setStockNum 設定庫存,或者使用 rdm 設定庫存

JMeter 配置壓力測驗腳本


測驗結果:

訪問 127.0.0.1:8080/successNum, 查看用戶實際搶購成功的商品數量:

???? 為什么用戶實際搶購數量為 1422 呢? 遠超于庫存 100 ,出現了超賣情況,如果實際應用中出現這樣的錯誤,那跟用戶怕是講不清了 …

原因分析:
從上面測驗結果,我們知道,高并發請求: 127.0.0.1:8080/grabBuy, 就會出現超賣現象,
其原因其實就是是庫存數量product_stock的讀和寫操作不在同一個原子操作上,導致類似不可重復讀的現象,可以類比多執行緒的問題,
什么是原子性呢?
在化學反應中,原子是不能再分的,在計算機中也就是某個操作不可分割的,就叫原子性,比如 a=0;(a非long和double型別) 這個操作是不可分割的,那么我們說這個操作時原子操作,再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作,非原子操作都會存在執行緒安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作,一個操作是原子操作,那么我們稱它具有原子性,
什么是不可重復讀呢?
在學習資料庫相關知識的時候,我們知道了事務的隔離級別:丟失更新 、臟讀、不可重復讀、幻讀,其中不可重復讀就是在同一事務中,多次讀取同一資料但是回傳不同的結果,也就是有其他事務更改了這些資料,
解決辦法:
提供兩種解決方法:
①: 通過redis事務解決超賣問題
@GetMapping("/grabBuy")
public String grabBuy() {
/*
* 解決商品超賣 方法一
* */
redisTemplate.setEnableTransactionSupport(true);
List<Object> result = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations redisOperations) throws DataAccessException {
// 監聽
redisOperations.watch("product_stock");
Integer product_stock = (Integer)redisOperations.opsForValue().get("product_stock");
// 開啟事務
redisOperations.multi();
// 必要的空查詢
redisOperations.opsForValue().get("product_stock");
product_stock = product_stock - 1;
if (product_stock < 0) {
return null;
}
// 剩余庫存存入
redisOperations.opsForValue().set("product_stock", product_stock);
return redisOperations.exec();
}
});
if (result != null && result.size() > 0) {
return "減少庫存成功,共減少" + successNum.incrementAndGet();
}
return "庫存不足";
}
②:通過使用 redisson 的 Rlock 加鎖方式解決超賣問題
pom.xml 引入依賴包:
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.6</version>
</dependency>
秒殺代碼撰寫:
@GetMapping("/grabBuy")
public String grabBuy() {
/*
* 解決商品超賣 方法二
* */
RLock rLock = redissonClient.getLock("product");
try {
rLock.lock();
Integer product_num = (Integer)redisTemplate.opsForValue().get("product_stock");
System.out.println(product_num);
if (product_num < 1) {
return "庫存不足!";
}
// 自減一
redisTemplate.opsForValue().decrement("product_stock");
return "減少庫存成功,共減少" + successNum.incrementAndGet();
} finally {
rLock.unlock();
}
}
注意: redissonClient.getLock() 括號內的引數千萬不能取redis資料庫臉面已經含有的鍵名,否則報如下錯誤,我找半天才發現這里的錯誤,
org.redisson.client.RedisException: ERR Error running script (call to f_3ffe249c16dee540ac8ab32e39bd408626b9aff7): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value . channel: [id: 0xfc81693b, L:/127.0.0.1:11732 - R:localhost/127.0.0.1:6379] command: (EVAL), params: [if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('h..., 2, product_stock, redisson_lock__channel:{product_stock}, 0, 30000, 93656223-0013-4a55-a2d6-06ec7df0cda2:71]
結果展示:


成功解決超賣問題!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/272168.html
標籤:其他

