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

方案概述

應用場景

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

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

秒殺過程中多次訪問數(shù)據(jù)庫,下單通常是利用行級鎖進行訪問限制,搶到鎖才能查詢數(shù)據(jù)庫和下單。但是秒殺時的大量訂單請求,會導致數(shù)據(jù)庫訪問阻塞。

解決方案

利用分布式緩存服務(DCS)的Redis作為數(shù)據(jù)庫的緩存,客戶端訪問Redis進行庫存查詢和下單操作,具有以下優(yōu)勢:

  • Redis提供很高的讀寫速度和并發(fā)性能,可以滿足電商秒殺系統(tǒng)高并發(fā)的需求。
  • Redis支持主備、集群等高可用架構, 支持數(shù)據(jù)持久化,即使服務器宕機也可以恢復數(shù)據(jù)。
  • Redis支持事務和原子性操作,可以保證秒殺操作的一致性和正確性。
  • 利用Redis緩存商品和用戶信息,減輕數(shù)據(jù)庫的壓力,提高系統(tǒng)的性能。

本篇文檔示例中,用Redis中的hash結構表示商品信息。total表示總數(shù),booked表示下單數(shù),remain表示剩余商品數(shù)量。

“product”: {
“total”: 200
“booked”:0
“remain”:200
}
 

扣量時,服務器通過請求Redis獲取下單資格。Redis為單線程模型,lua可以保證多個命令的原子性。通過如下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緩存實例,且狀態(tài)為“運行中”。
  • 客戶端所在服務器與DCS緩存實例網(wǎng)絡互通:
    • 客戶端與Redis實例所在VPC為同一VPC

      同一VPC內網(wǎng)絡默認互通。

    • 客戶端與Redis實例所在VPC為相同region下的不同VPC

      如果客戶端與Redis實例不在相同VPC中,可以通過建立VPC對等連接方式連通網(wǎng)絡,具體請參考:緩存實例是否支持跨VPC訪問?

    • 客戶端與Redis實例所在VPC不在相同region

      如果客戶端服務器和Redis實例不在同一region,僅支持通過 云專線 打通網(wǎng)絡,請參考云專線。

    • 公網(wǎng)訪問

      客戶端公網(wǎng)訪問Redis 4.0/5.0/6.0實例時,需要開啟實例公網(wǎng)訪問開關,具體請參考開啟Redis 4.0/5.0/6.0公網(wǎng)訪問并獲取公網(wǎng)訪問地址。

  • 客戶端所在服務器已安裝JDK1.8以上版本和Intellij IDEA開發(fā)工具,下載jedis客戶端(點此處下載jar包)。

    本文檔下載的開發(fā)工具和客戶端僅為示例,您可以選擇其它類型的工具和客戶端。

實施步驟

  1. 在服務器上運行Intellij IDEA,創(chuàng)建一個MAVEN工程,為示例代碼創(chuàng)建一個SecondsKill.java文件,pom.xml文件中引用Jedis:

     

    <dependency>
          <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
          <version>4.2.0</version>
    </dependency>

  2. 編譯并運行以下demo,該示例以Java語言實現(xiàn)。

     

    示例中的Redis連接地址和端口需要根據(jù)實際獲取的值進行修改。
    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實例連接地址和端口需替換為實際獲取的值
    		JedisPool pool = new JedisPool(config, "127.0.0.1", 6379);
    		Jedis jedis = null;
    		try {
    			jedis = pool.getResource();
                            jedis.auth("password");   //配置實例的連接密碼,免密訪問的實例無需填寫
    			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個
    			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í)行結果:

    total:200
    booked:15
    remain:185