方案概述
應(yīng)用場(chǎng)景
電商秒殺是一種網(wǎng)上競(jìng)拍活動(dòng),通常商家會(huì)在平臺(tái)釋放少量稀缺商品,吸引大量客戶,平臺(tái)會(huì)收到平時(shí)數(shù)十倍甚至上百倍的下單請(qǐng)求,但是只有少數(shù)客戶可以下單成功。電商秒殺系統(tǒng)的分流過程可以分為以下幾個(gè)步驟:
- 用戶請(qǐng)求進(jìn)入系統(tǒng):當(dāng)用戶發(fā)起秒殺請(qǐng)求時(shí),請(qǐng)求會(huì)首先進(jìn)入 負(fù)載均衡 服務(wù)器。
- 負(fù)載均衡:負(fù)載均衡服務(wù)器會(huì)根據(jù)一定的算法將請(qǐng)求分發(fā)給后端多臺(tái)服務(wù)器,以達(dá)到負(fù)載均衡的目的。負(fù)載均衡算法可以采用輪詢、隨機(jī)、最少連接數(shù)等方式。
- 業(yè)務(wù)邏輯處理:后端服務(wù)器接收到請(qǐng)求后,進(jìn)行業(yè)務(wù)邏輯處理,并根據(jù)請(qǐng)求的商品數(shù)量、用戶身份等信息進(jìn)行校驗(yàn)。
- 庫(kù)存扣減:如果庫(kù)存充足,后端服務(wù)器會(huì)進(jìn)行庫(kù)存扣減操作,并生成訂單信息,返回給用戶秒殺成功的信息;如果庫(kù)存不足,則返回給用戶秒殺失敗的信息。
- 訂單處理:后端服務(wù)器會(huì)將訂單信息保存到 數(shù)據(jù)庫(kù) 中,并進(jìn)行異步處理,例如發(fā)送 消息通知 用戶訂單狀態(tài)。
- 緩存更新:后端服務(wù)器會(huì)更新緩存中的商品庫(kù)存信息,以便處理下一次秒殺請(qǐng)求。
秒殺過程中多次訪問數(shù)據(jù)庫(kù),下單通常是利用行級(jí)鎖進(jìn)行訪問限制,搶到鎖才能查詢數(shù)據(jù)庫(kù)和下單。但是秒殺時(shí)的大量訂單請(qǐng)求,會(huì)導(dǎo)致數(shù)據(jù)庫(kù)訪問阻塞。
解決方案
利用分布式緩存服務(wù)(DCS)的Redis作為數(shù)據(jù)庫(kù)的緩存,客戶端訪問Redis進(jìn)行庫(kù)存查詢和下單操作,具有以下優(yōu)勢(shì):
- Redis提供很高的讀寫速度和并發(fā)性能,可以滿足電商秒殺系統(tǒng)高并發(fā)的需求。
- Redis支持主備、集群等高可用架構(gòu), 支持?jǐn)?shù)據(jù)持久化,即使服務(wù)器宕機(jī)也可以恢復(fù)數(shù)據(jù)。
- Redis支持事務(wù)和原子性操作,可以保證秒殺操作的一致性和正確性。
- 利用Redis緩存商品和用戶信息,減輕數(shù)據(jù)庫(kù)的壓力,提高系統(tǒng)的性能。
本篇文檔示例中,用Redis中的hash結(jié)構(gòu)表示商品信息。total表示總數(shù),booked表示下單數(shù),remain表示剩余商品數(shù)量。
“product”: {
“total”: 200
“booked”:0
“remain”:200
}
扣量時(shí),服務(wù)器通過請(qǐng)求Redis獲取下單資格。Redis為單線程模型,lua可以保證多個(gè)命令的原子性。通過如下lua腳本完成扣量。
local n = tonumber(ARGV[1]) if not n or n == 0 then return 0 end local vals = redis.call(\"HMGET\", KEYS[1], \"total\", \"booked\", \"remain\"); local booked = tonumber(vals[2]) local remain = tonumber(vals[3]) if booked <= remain then redis.call(\"HINCRBY\", KEYS[1], \"booked\", n) redis.call(\"HINCRBY\", KEYS[1], \"remain\", -n) return n; end return 0
前提條件
- 已創(chuàng)建DCS緩存實(shí)例,且狀態(tài)為“運(yùn)行中”。
- 客戶端所在服務(wù)器與DCS緩存實(shí)例網(wǎng)絡(luò)互通:
- 客戶端與Redis實(shí)例所在VPC為同一VPC
- 客戶端與Redis實(shí)例所在VPC為相同region下的不同VPC
如果客戶端與Redis實(shí)例不在相同VPC中,可以通過建立VPC對(duì)等連接方式連通網(wǎng)絡(luò),具體請(qǐng)參考:緩存實(shí)例是否支持跨VPC訪問?。
- 客戶端與Redis實(shí)例所在VPC不在相同region
如果客戶端服務(wù)器和Redis實(shí)例不在同一region,僅支持通過 云專線 打通網(wǎng)絡(luò),請(qǐng)參考云專線。
- 公網(wǎng)訪問
客戶端公網(wǎng)訪問Redis 4.0/5.0/6.0實(shí)例時(shí),需要開啟實(shí)例公網(wǎng)訪問開關(guān),具體請(qǐng)參考開啟Redis 4.0/5.0/6.0公網(wǎng)訪問并獲取公網(wǎng)訪問地址。
- 客戶端所在服務(wù)器已安裝JDK1.8以上版本和Intellij IDEA開發(fā)工具,下載jedis客戶端(點(diǎn)此處下載jar包)。
本文檔下載的開發(fā)工具和客戶端僅為示例,您可以選擇其它類型的工具和客戶端。
實(shí)施步驟
- 在服務(wù)器上運(yùn)行Intellij IDEA,創(chuàng)建一個(gè)MAVEN工程,為示例代碼創(chuàng)建一個(gè)SecondsKill.java文件,pom.xml文件中引用Jedis:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.2.0</version> </dependency> - 編譯并運(yùn)行以下demo,該示例以Java語(yǔ)言實(shí)現(xiàn)。
示例中的Redis連接地址和端口需要根據(jù)實(shí)際獲取的值進(jìn)行修改。
package com.huawei.demo; import java.util.ArrayList; import java.util.*; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class SecondsKill { private static void InitProduct(Jedis jedis) { jedis.hset("product", "total", "200"); jedis.hset("product", "booked", "0"); jedis.hset("product","remain", "200"); } private static String LoadLuaScript(Jedis jedis) { String lua = "local n = tonumber(ARGV[1])\n" + "if not n or n == 0 then\n" + "return 0\n" + "end\n" + "local vals = redis.call(\"HMGET\", KEYS[1], \"total\", \"booked\", \"remain\");\n" + "local booked = tonumber(vals[2])\n" + "local remain = tonumber(vals[3])\n" + "if booked <= remain then\n" + "redis.call(\"HINCRBY\", KEYS[1], \"booked\", n)\n" + "redis.call(\"HINCRBY\", KEYS[1], \"remain\", -n)\n" + "return n;\n" + "end\n" + "return 0"; String scriptLoad = jedis.scriptLoad(lua); return scriptLoad; } public static void main(String[] args) { JedisPoolConfig config = new JedisPoolConfig(); // 最大連接數(shù) config.setMaxTotal(30); // 最大連接空閑數(shù) config.setMaxIdle(2); // 連接Redis,Redis實(shí)例連接地址和端口需替換為實(shí)際獲取的值 JedisPool pool = new JedisPool(config, "127.0.0.1", 6379); Jedis jedis = null; try { jedis = pool.getResource(); jedis.auth("password"); //配置實(shí)例的連接密碼,免密訪問的實(shí)例無(wú)需填寫 System.out.println(jedis); // 初始化產(chǎn)品信息 InitProduct(jedis); // 存入lua腳本 String scriptLoad = LoadLuaScript(jedis); List<String> keys = new ArrayList<>(); List<String> vals = new ArrayList<>(); keys.add("product"); //下單15個(gè) int num = 15; vals.add(String.valueOf(num)); //執(zhí)行l(wèi)ua腳本 jedis.evalsha(scriptLoad, keys, vals); System.out.println("total:"+jedis.hget("product", "total")+"\n"+"booked:"+jedis.hget("product", "booked")+"\n"+"remain:"+jedis.hget("product","remain")); } catch (Exception ex) { ex.printStackTrace(); } finally { if (jedis != null) { jedis.close(); } } } }執(zhí)行結(jié)果:
total:200 booked:15 remain:185