華為云計(jì)算 云知識(shí) 使用分布式緩存服務(wù)DCS實(shí)現(xiàn)電商秒殺功能
使用分布式緩存服務(wù)DCS實(shí)現(xiàn)電商秒殺功能

方案概述

應(yīng)用場(chǎng)景

電商秒殺是一種網(wǎng)上競(jìng)拍活動(dòng),通常商家會(huì)在平臺(tái)釋放少量稀缺商品,吸引大量客戶,平臺(tái)會(huì)收到平時(shí)數(shù)十倍甚至上百倍的下單請(qǐng)求,但是只有少數(shù)客戶可以下單成功。電商秒殺系統(tǒng)的分流過程可以分為以下幾個(gè)步驟:

  1. 用戶請(qǐng)求進(jìn)入系統(tǒng):當(dāng)用戶發(fā)起秒殺請(qǐng)求時(shí),請(qǐng)求會(huì)首先進(jìn)入 負(fù)載均衡 服務(wù)器。
  2. 負(fù)載均衡:負(fù)載均衡服務(wù)器會(huì)根據(jù)一定的算法將請(qǐng)求分發(fā)給后端多臺(tái)服務(wù)器,以達(dá)到負(fù)載均衡的目的。負(fù)載均衡算法可以采用輪詢、隨機(jī)、最少連接數(shù)等方式。
  3. 業(yè)務(wù)邏輯處理:后端服務(wù)器接收到請(qǐng)求后,進(jìn)行業(yè)務(wù)邏輯處理,并根據(jù)請(qǐng)求的商品數(shù)量、用戶身份等信息進(jìn)行校驗(yàn)。
  4. 庫(kù)存扣減:如果庫(kù)存充足,后端服務(wù)器會(huì)進(jìn)行庫(kù)存扣減操作,并生成訂單信息,返回給用戶秒殺成功的信息;如果庫(kù)存不足,則返回給用戶秒殺失敗的信息。
  5. 訂單處理:后端服務(wù)器會(huì)將訂單信息保存到 數(shù)據(jù)庫(kù) 中,并進(jìn)行異步處理,例如發(fā)送 消息通知 用戶訂單狀態(tài)。
  6. 緩存更新:后端服務(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

      同一VPC內(nèi)網(wǎng)絡(luò)默認(rèn)互通。

    • 客戶端與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í)施步驟

  1. 在服務(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>

  2. 編譯并運(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