電商秒殺系統:服務器集群、分布式緩存redis、lua實現單品限流和限制重復購買、搶購方法冪、搶購失敗回滾、雪花算法、IP限流防刷


服務器集群+IP限流防刷

Nginx負載均衡集群配置

參考:

Nginx官網

Nginx中文文檔

Nginx搭建負載均衡集群

Nginx集群(負載均衡)

 

Nginx版本: 1.17.1

配置文件路徑:nginx-1.17.1 \ conf \ nginx.conf

打開文件后具體配置:

  • 在 http 下添加 upstream(上游)節點,名稱定義為:seckillagrreate,
  • 下面在添加兩個server節點:server  IP地址 : 端口 ;
  • http {
        # 秒殺聚合服務負載均衡
        upstream seckillagrreate{
            server localhost:5006;
            server localhost:5010;
        }
    }

Nginx IP限流防刷

Nginx的IP常用限制

  • IP請求限制:limit_req_zone,參考:HTTP Limit Requests模塊*
  • IP並發數:   limit_conn_zone
  • IP下載速度:limit_rate

這里暫用IP請求限制:也是在上面的配置文件中,增加2行紅色的代碼,限制每個IP地址1秒內只處理一次請求,如下:

http {
    # ip 請求限制
 limit_req_zone $binary_remote_addr zone=seckill:10m rate=1r/s;

    server {
        listen       8082;
        server_name  localhost;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;
        
        location / {
            #root   html;
            #index  index.html index.htm;
            limit_req zone=seckill;
            proxy_pass  https://seckillagrreate;
        }
}

 

分布式緩存redis

參考:

Redis中文文檔

Redis Desktop Manager :可視化管理工具、圖形化管理工具、可視化客戶端、集群管理工具

ASP.NET Core 中的分布式緩存  --備注:.net封裝好的reidis只能增刪改查操作,而秒秒刪項目還需要單品限流和限制重復購買,所以這里不使用.net封裝好的redis

redis特點

  • 單線程
  • 分布式

問題

秒殺聚合服務器緩存秒殺庫存時是用內存緩存,但是聚合服務做集群時還繼續使用內存緩存,每台服務器的緩存的存庫是獨立的,會存在庫存不一致、超賣問題。

改進

引入redis分布式緩存,聚合服務集群時的多台服務器分布式庫存,避免超賣。

項目中使用redis

在公共層 Commons 中引入nuget包:CSRedisCore

在公共層 Commons 中添加redis擴展類:RedisServiceCollectionExtensions

using CSRedis;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;

namespace RuanMou.Projects.Commons.Caches
{
    /// <summary>
    /// ServiceCollection Redis擴展
    /// </summary>
    public static class RedisServiceCollectionExtensions
    {
        /// <summary>
        ///  注冊分布式Redis緩存
        /// </summary>
        /// <typeparam name="connectionString"></typeparam>
        /// <returns></returns>
        public static IServiceCollection AddDistributedRedisCache(this IServiceCollection services,string connectionString)
        {
            // 1、創建redis客戶端實例
            // var csredis = new CSRedisClient("127.0.0.1:6379,password=,defaultDatabase=2,poolsize=50,connectTimeout=5000,syncTimeout=10000,prefix=cs_redis_");
            var csredis = new CSRedisClient(connectionString);
            // 2、注冊RedisClient到IOC
            services.AddSingleton(csredis);

            // 3、添加到redis幫助類
            RedisHelper.Initialization(csredis);//初始化
            return services;
        }

        /// <summary>
        ///  注冊分布式Redis集群緩存
        /// </summary>
        /// <typeparam name="connectionString"></typeparam>
        /// <returns></returns>
        public static IServiceCollection AddDistributedRedisCache(this IServiceCollection services, string[] connectionString)
        {
            // 1、創建redis客戶端實例
            var csredis = new CSRedisClient((d) => { return ""; },connectionString);

            // 2、注冊RedisClient到IOC
            services.AddSingleton(csredis);

            // 3、添加到redi幫助類
            RedisHelper.Initialization(csredis);//初始化
            return services;
        }
    }
}
View Code

在秒殺聚合服務中添加Redis扣減庫存類:RedisSeckillStockCache

redis存儲庫存時使用的數據類型是 哈希(Hashe)

先扣減庫存后,再判斷庫存數量,減少一次網絡 IO,如果庫存小於0,再調用Lua編寫的失敗回滾函數

using RuanMou.Projects.Commons.Exceptions;
using RuanMou.Projects.SeckillAggregateServices.Models.SeckillService;
using RuanMou.Projects.SeckillAggregateServices.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace RuanMou.Projects.SeckillAggregateServices.Caches.SeckillStock
{
    /// <summary>
    /// 秒殺庫存redis緩存
    /// </summary>
    public class RedisSeckillStockCache : ISeckillStockCache
    {
        /// <summary>
        /// 秒殺微服務客戶端
        /// </summary>
        private readonly ISeckillsClient seckillsClient;

        public RedisSeckillStockCache(ISeckillsClient seckillsClient)
        {
            this.seckillsClient = seckillsClient;
        }
        /// <summary>
        /// 根據商品編號獲取秒殺庫存
        /// </summary>
        public int GetSeckillStocks(int ProductId)
        {
            return Convert.ToInt32(RedisHelper.HGet(Convert.ToString(ProductId), "SeckillStock"));
        }

        /// <summary>
        /// 秒殺庫存加載到redis中
        /// </summary>
        public void SkillStockToCache()
        {
            // 1、查詢所有秒殺活動
            List<Seckill> seckills = seckillsClient.GetSeckills();

            // 2、加載秒殺商品
            foreach (var seckill in seckills)
            {
                // 3、存數秒殺庫存
                bool flag = RedisHelper.HSet(Convert.ToString(seckill.ProductId), "SeckillStock", seckill.SeckillStock);
                // 4、存儲限制秒殺購買數量
                bool flag2 = RedisHelper.HSet(Convert.ToString(seckill.ProductId), "SeckillLimit", seckill.SeckillLimit);

                // 3.1 存儲到redis失敗
                /*if (!flag && !flag2)
                {
                    throw new BizException("redis存儲數據失敗");
                }*/

                // flag // flag2 判斷key是否存在
            }
        }

        /// <summary>
        /// redis扣減庫存
        /// </summary>
        /// <param name="ProductId"></param>
        /// <param name="ProductCount"></param>
        public void SubtractSeckillStock(int ProductId, int ProductCount)
        {
            //先扣減庫存后,再判斷庫存數量,減少一次網絡IO
            //1 先扣減庫存
            long seckillStock = RedisHelper.HIncrBy(Convert.ToString(ProductId), "SeckillStock", -ProductCount);
            //2 再判斷存儲
            if (seckillStock < 0)
            {
                throw new BizException("秒殺已結束");
            }
        }
    }
}
View Code

 

在秒殺聚合服務的Starup中指定redis連接地址、引用redis擴展類:

// 6.1 使用redis分布式緩存
services.AddDistributedRedisCache("10.96.0.6:6379, password =, defaultDatabase = 2, poolsize = 50, connectTimeout = 5000, syncTimeout = 10000, prefix = seckill_stock_");// k8s redis

// 7.1 使用秒殺redis庫存緩存
services.AddRedisSeckillStockCache();

 

 

lua實現單品限流+限制重復購買、搶購失敗回滾

參考:

Lua官網

Redis Lua 腳本

Lua菜鳥教程

Lua概念

  • 原子操作:要么同時成功,要么同時失敗
  • lua文件里面是創建Redis函數的腳本,函數包復雜業務,加載到redis后就會創建函數存在redis內存中,后面程序使用redis時直接調用函數就好。

在秒殺聚合服務中實現 

秒殺Lua文件,實現單品限流和限制重復購買:SeckillLua.lua

--[[
    1、函數定義
]]--
--1、單品限流
local function seckillLimit()
local seckillLimitKey = ARGV[2];
-- 1、獲取單品已經請求數量
local limitCount = tonumber(redis.call('get',seckillLimitKey) or "0");
local requestCountLimits = tonumber(ARGV[4]); --限制的請求數量
local seckillLimitKeyExpire = tonumber(ARGV[5]); --2秒過期
if limitCount + 1 > requestCountLimits then --超出限流大小
return 0,seckillLimitKeyExpire.."內只能請求"..requestCountLimits.."";  --失敗
else --請求數+1,並設置過期時間
redis.call('INCRBY',seckillLimitKey,"1")
redis.call('expire',seckillLimitKey,seckillLimitKeyExpire)
return 1; --成功
end
end

--2、記錄訂單號:目的:創建訂單方法冪等性,調用方網絡超時可以重復調用,存在訂單號直接返回搶購成功,不至於超賣
local function recordOrderSn()
local requestIdKey = ARGV[6]; -- 訂單號key
local orderSn = ARGV[7]; -- 訂單號
local hasOrderSn = tostring(redis.call('get',requestIdKey) or "");
if string.len(hasOrderSn) == 0 then
-- 存儲訂單號
redis.call('set',requestIdKey,orderSn);
return 1; -- 設置成功
else
return 0,"不能重復下單"; --失敗
end
end

--3、用戶購買限制
local function userBuyLimit()
local userBuyLimitKey = ARGV[1]; -- 購買限制key
local productKey =KEYS[1]; --商品key
local productCount = tonumber(ARGV[3]);-- 商品數量
-- 1、用戶已經購買數量
local userHasBuyCount = tonumber(redis.call('hget',userBuyLimitKey,"UserBuyLimit") or "0");
-- 2、獲取限制的數量
local seckillLimit = tonumber(redis.call('hget',productKey,"SeckillLimit") or "0");
if userHasBuyCount + 1 > seckillLimit then --超出購買數量
return 0,"該商品只能購買"..seckillLimit..""; --失敗
else --請求數+1,並設置過期時間
redis.call('HINCRBY',userBuyLimitKey,'UserBuyLimit',productCount)
return 1; --成功
end
end


--4、扣減庫存
local function subtractSeckillStock()
local productKey =KEYS[1]; --商品key
local productCount = tonumber(ARGV[3]);--商品數量
-- 1.1、扣減庫存
local lastNum = redis.call('HINCRBY',productKey,"SeckillStock",-productCount);
-- 1.2、判斷庫存是否完成
if lastNum < 0 then
return 0,"秒殺已結束"; --失敗
else
return 1; --成功
end
end

--[[
    2、函數調用
]]--
--1、單品限流
local status,msg = seckillLimit();
if status == 0 then
return msg
end
--2、記錄訂單號;
local status,msg = recordOrderSn();
if status == 0 then
return msg
end

--3、用戶購買限制
status,msg = userBuyLimit();
if status == 0 then
return msg
end
--4、扣減秒殺庫存
status,msg = subtractSeckillStock();
if status == 0 then
return msg
end
-- 返回成功標識
return 1;
View Code

秒殺回滾Lua文件,反向操作:SeckillLuaCallback.lua

--[[
    1、函數定義
]]--
--1、刪除記錄訂單號:目的:創建訂單方法冪等性,調用方網絡超時可以重復調用,存在訂單號直接返回搶購成功,不至於超賣
local function delRecordOrderSn()
local requestIdKey = ARGV[3]; -- 訂單號key
local orderSn = ARGV[4]; -- 訂單號
--刪除訂單號
redis.call('del',requestIdKey)
end

--2、刪除用戶購買限制
local function delUserBuyLimit()
local userBuyLimitKey = ARGV[1]; -- 購買限制key
local productKey =KEYS[1]; --商品key
local productCount = tonumber(ARGV[2]);-- 商品數量
redis.call('HINCRBY',userBuyLimitKey,'UserBuyLimit',-productCount)
end

--3、恢復庫存
local function recoverSeckillStock()
local productKey =KEYS[1]; --商品key
local productCount = tonumber(ARGV[2]);--商品數量
-- 3.1、恢復庫存
redis.call('HINCRBY',productKey,"SeckillStock",productCount);
end


--[[
    2、函數調用
]]--
--1、刪除記錄訂單號;
delRecordOrderSn();

--2、撤銷用戶購買限制
delUserBuyLimit();

--3、恢復秒殺庫存
recoverSeckillStock();
View Code

 

c#把Lua文件加載到Redis中,在redis中創建函數:SeckillLuaHostedService.cs

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace RuanMou.Projects.SeckillAggregateServices.Caches.SeckillStock
{
    /// <summary>
    /// 服務啟動加載秒殺Lua文件
    /// </summary>
    public class SeckillLuaHostedService : IHostedService
    {
        private readonly IMemoryCache memoryCache;

        public SeckillLuaHostedService(IMemoryCache memoryCache)
        {
            this.memoryCache = memoryCache;
        }

        /// <summary>
        /// 加載秒殺庫存緩存
        /// </summary>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task StartAsync(CancellationToken cancellationToken)
        {
            try
            {
                Console.WriteLine("加載執行lua文件到redis中");
                // 1、加載lua到redis
                FileStream fileStream = new FileStream(@"Luas/SeckillLua.lua", FileMode.Open);
                using (StreamReader reader = new StreamReader(fileStream))
                {
                    string line = reader.ReadToEnd();
                    string luaSha = RedisHelper.ScriptLoad(@line);

                    // 2、保存luaSha到緩存中
                    memoryCache.Set<string>("luaSha", luaSha);
                }

                Console.WriteLine("加載回滾lua文件到redis中");
                // 1、加載lua到redis
                FileStream fileStreamCallback = new FileStream(@"Luas/SeckillLuaCallback.lua", FileMode.Open);
                using (StreamReader reader = new StreamReader(fileStreamCallback))
                {
                    string line = reader.ReadToEnd();
                    string luaSha = RedisHelper.ScriptLoad(@line);

                    // 2、保存luaShaCallback到緩存中
                    memoryCache.Set<string>("luaShaCallback", luaSha);
                }

            }
            catch (Exception e)
            {
                Console.WriteLine($"lua文件異常:{e.Message}");
            }

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}
View Code

在Starup中加載SeckillLuaHostedService類

// 9、加載seckillLua文件
services.AddHostedService<SeckillLuaHostedService>();

訂單控制器OrderController中創建訂單方法CreateOrder,根據函數名調用redis上的函數。

搶購失敗回滾:

        /// <summary>
        /// 4.5、創建訂單(redis + 消息隊列 + lua + 方法冪等 + 失敗回滾)
        /// </summary>
        /// <param name="orderDto"></param>
        [HttpPost]
        public PaymentDto CreateOrder(SysUser sysUser, [FromForm] OrderPo orderPo)
        {
            // 1、秒殺參數准備
            string ProductKey = Convert.ToString(orderPo.ProductId);// 商品key
            string SeckillLimitKey = "seckill_stock_:SeckillLimit" + orderPo.ProductCount; // 單品限流key
            string UserBuyLimitKey = "seckill_stock_:UserId" + sysUser.UserId + "ProductId" + orderPo.ProductId;// 用戶購買限制key
            int productCount = orderPo.ProductCount; // 購買商品數量
            int requestCountLimits = 60000; // 單品限流數量
            int seckillLimitKeyExpire = 60;// 單品限流時間:單位秒
            string requestIdKey = "seckill_stock_:" + orderPo.RequestId; // requestIdKey
            string orderSn = OrderUtil.GetOrderCode();// 訂單號
                                                      //string orderSn = distributedOrderSn.CreateDistributedOrderSn(); // 分布式訂單號

            // 2、執行秒殺
            var SeckillResult = RedisHelper.EvalSHA(memoryCache.Get<string>("luaSha"), ProductKey, UserBuyLimitKey, SeckillLimitKey, productCount, requestCountLimits, seckillLimitKeyExpire, requestIdKey, orderSn);
            if (!SeckillResult.ToString().Equals("1"))
            {
                throw new BizException(SeckillResult.ToString());
            }

            try
            {
                // throw new Exception("222");
                // 3、發送訂單消息到rabbitmq 發送失敗,消息回滾
                SendOrderCreateMessage(sysUser.UserId, orderSn, orderPo);
            }
            catch (Exception)
            {
                // 3.1 秒殺回滾
                RedisHelper.EvalSHA(memoryCache.Get<string>("luaShaCallback"), ProductKey, UserBuyLimitKey, productCount, requestIdKey, orderSn);

                // 3.2 搶購失敗
                throw new BizException("搶購失敗");

                // 3.3 少賣問題是允許的,100個商品 99個 100 個 100票
            }

            // 4、創建支付信息
            PaymentDto paymentDto = new PaymentDto();
            paymentDto.OrderSn = orderSn;
            paymentDto.OrderTotalPrice = orderPo.OrderTotalPrice;
            paymentDto.UserId = sysUser.UserId;
            paymentDto.ProductId = orderPo.ProductId;
            paymentDto.ProductName = orderPo.ProductName;

            return paymentDto;
        }
View Code

搶購方法冪

冥等概念

參考:

冥等--百度百科

CAP冥等性

在編程中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。其實就是多次執行時檢查是否已經處理過,處理過的就不再處理了。

單個頁面冥等實現

前端頁面限制避免用戶重復下單,不能限制多個頁面,同一次請求只能下單一次。就算用戶重復下單也沒事,Lua文件中也已經限制了用戶重復購買。

在秒殺前台SeckillFronts中JavaScript腳本:用到了瀏覽器緩存sessionStorage+請求ID+時間戳

訂單腳本:order.js

// 訂單確認頁面
$(function () {
    // 1、下單
    $(".btn-order").click(function () {
        // 判斷是否登錄
        if (!isHasLogin()) {
            return;
        }

        var ProductId = $("#ProductId").val();
        var ProductUrl = $("#ProductUrl").val();
        var ProductTitle = $("#ProductTitle").val();
        var ProductPrice = $("#ProductPrice").val();
        var ProductCount = $("#ProductCount").val();
         var orderUrl = "https://localhost:5006/api/Order/";
        // var orderUrl = "http://116.62.212.16:5006/api/Order/";
        $.ajax({
            method: "POST",
            url: orderUrl,
            dataType: "json",
            data: {
                "ProductId": ProductId,
                "ProductUrl": ProductUrl,
                "ProductName": ProductTitle,
                "OrderTotalPrice": ProductPrice,
                "ProductCount": ProductCount,
                "RequestId": getRequestId()
            },
            success: function (result) {
                if (result.ErrorNo == "0") {
                    // 1、跳轉到支付頁面
                    var resultDic = result.ResultDic;
                    location.href = "/Payment/Index?OrderId=" + resultDic.OrderId + "&OrderSn=" + resultDic.OrderSn + "&OrderTotalPrice=" + resultDic.OrderTotalPrice + "&UserId=" + resultDic.UserId + "&ProductId=" + resultDic.ProductId + "&ProductName=" + resultDic.ProductName +"";
                } else {
                    alert(result.ErrorInfo);
                }
            }
        })
    })
})

//創建請求唯一id 方法:時間戳 + UserId
function createRequestId(UserId) {
   // return Number(Math.random().toString().substr(3, length) + Date.now() + UserId).toString(37);
    return (Date.now() + UserId).toString();
}

// 保存請求id
function saveRequestId(userId, requestId) {
    // 1、存儲requestId
    sessionStorage.setItem(userId, requestId);
}

// 獲取請求id
function getRequestId() {
    // 1、獲取userId
    var user = getCache("user");

    // 2、從sessionStorage中獲取requestId
    var requestId = sessionStorage.getItem(user.UserId);

    // 3、判斷requestId是否存在
    if (!requestId) {
        requestId =  createRequestId(user.UserId);
    }
    // 4、存儲requestId
    saveRequestId(user.UserId, requestId);
    return requestId;
}
View Code

緩存腳本:cache.js

// 緩存js
//封裝過期控制代碼
function setCache(key, value, exp) {
    var time = new Date();
    time.setSeconds(exp);
    var expTime = time.getTime();
    localStorage.setItem(key, JSON.stringify({ data: value, time: expTime }));
}
function getCache(key) {
    var data = localStorage.getItem(key);
    console.log(data);
    if (data == null) {
        // 返回空對象
        return {};
    }
    var dataObj = JSON.parse(data);
    console.log(new Date().getTime());
    console.log(dataObj.time);
    if (new Date().getTime() > dataObj.time) {
        console.log('信息已過期');
        localStorage.removeItem(key);
        return {};
    } else {
        var dataObjDatatoJson = dataObj.data;
        return dataObjDatatoJson;
    }
}

function removeCache(key) {
    localStorage.removeItem(key);
}
View Code

雪花算法

在集群時避免訂單號重復,生成唯一ID,在公共層Commons的Distributes中增加

分布式訂單:DistributedOrderSn

using System;
using System.Collections.Generic;
using System.Text;

namespace RuanMou.Projects.Commons.Distributes
{
    /// <summary>
    /// 分布式訂單
    /// </summary>
    public class DistributedOrderSn
    {

        private readonly SnowflakeId snowflakeId;

        public DistributedOrderSn(SnowflakeId snowflakeId)
        {
            this.snowflakeId = snowflakeId;
        }

        /// <summary>
        /// 創建訂單號
        /// </summary>
        /// <returns></returns>
        public string CreateDistributedOrderSn()
        {
           // 1、可以選擇加前綴
           return Convert.ToString(snowflakeId.NextId());
        }
    }
}
View Code

雪花ID類:SnowflakeId

using System;
using System.Collections.Generic;
using System.Text;

namespace RuanMou.Projects.Commons.Distributes
{
    /// <summary>
    /// 雪花Id
    /// </summary>
    public class SnowflakeId
    {
        // 開始時間截 (new DateTime(2020, 1, 1).ToUniversalTime() - Jan1st1970).TotalMilliseconds
        private const long twepoch = 1577808000000L;

        // 機器id所占的位數
        private const int workerIdBits = 5;

        // 數據標識id所占的位數
        private const int datacenterIdBits = 5;

        // 支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數) 
        private const long maxWorkerId = -1L ^ (-1L << workerIdBits);

        // 支持的最大數據標識id,結果是31 
        private const long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

        // 序列在id中占的位數 
        private const int sequenceBits = 12;

        // 數據標識id向左移17位(12+5) 
        private const int datacenterIdShift = sequenceBits + workerIdBits;

        // 機器ID向左移12位 
        private const int workerIdShift = sequenceBits;


        // 時間截向左移22位(5+5+12) 
        private const int timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

        // 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095) 
        private const long sequenceMask = -1L ^ (-1L << sequenceBits);

        // 數據中心ID(0~31) 
        public long datacenterId { get; private set; }

        // 工作機器ID(0~31) 
        public long workerId { get; private set; }

        // 毫秒內序列(0~4095) 
        public long sequence { get; private set; }

        // 上次生成ID的時間截 
        public long lastTimestamp { get; private set; }


        /// <summary>
        /// 雪花ID
        /// </summary>
        /// <param name="datacenterId">數據中心ID</param>
        /// <param name="workerId">工作機器ID</param>
        public SnowflakeId(long datacenterId, long workerId)
        {
            if (datacenterId > maxDatacenterId || datacenterId < 0)
            {
                throw new Exception(string.Format("datacenter Id can't be greater than {0} or less than 0", maxDatacenterId));
            }
            if (workerId > maxWorkerId || workerId < 0)
            {
                throw new Exception(string.Format("worker Id can't be greater than {0} or less than 0", maxWorkerId));
            }
            this.workerId = workerId;
            this.datacenterId = datacenterId;
            this.sequence = 0L;
            this.lastTimestamp = -1L;
        }

        /// <summary>
        /// 獲得下一個ID
        /// </summary>
        /// <returns></returns>
        public long NextId()
        {
            lock (this)
            {
                long timestamp = GetCurrentTimestamp();
                if (timestamp > lastTimestamp) //時間戳改變,毫秒內序列重置
                {
                    sequence = 0L;
                }
                else if (timestamp == lastTimestamp) //如果是同一時間生成的,則進行毫秒內序列
                {
                    sequence = (sequence + 1) & sequenceMask;
                    if (sequence == 0) //毫秒內序列溢出
                    {
                        timestamp = GetNextTimestamp(lastTimestamp); //阻塞到下一個毫秒,獲得新的時間戳
                    }
                }
                else   //當前時間小於上一次ID生成的時間戳,證明系統時鍾被回撥,此時需要做回撥處理
                {
                    sequence = (sequence + 1) & sequenceMask;
                    if (sequence > 0)
                    {
                        timestamp = lastTimestamp;     //停留在最后一次時間戳上,等待系統時間追上后即完全度過了時鍾回撥問題。
                    }
                    else   //毫秒內序列溢出
                    {
                        timestamp = lastTimestamp + 1;   //直接進位到下一個毫秒                          
                    }
                    //throw new Exception(string.Format("Clock moved backwards.  Refusing to generate id for {0} milliseconds", lastTimestamp - timestamp));
                }

                lastTimestamp = timestamp;       //上次生成ID的時間截

                //移位並通過或運算拼到一起組成64位的ID
                var id = ((timestamp - twepoch) << timestampLeftShift)
                        | (datacenterId << datacenterIdShift)
                        | (workerId << workerIdShift)
                        | sequence;
                return id;
            }
        }

        /// <summary>
        /// 解析雪花ID
        /// </summary>
        /// <returns></returns>
        public static string AnalyzeId(long Id)
        {
            StringBuilder sb = new StringBuilder();

            var timestamp = (Id >> timestampLeftShift);
            var time = Jan1st1970.AddMilliseconds(timestamp + twepoch);
            sb.Append(time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss:fff"));

            var datacenterId = (Id ^ (timestamp << timestampLeftShift)) >> datacenterIdShift;
            sb.Append("_" + datacenterId);

            var workerId = (Id ^ ((timestamp << timestampLeftShift) | (datacenterId << datacenterIdShift))) >> workerIdShift;
            sb.Append("_" + workerId);

            var sequence = Id & sequenceMask;
            sb.Append("_" + sequence);

            return sb.ToString();
        }

        /// <summary>
        /// 阻塞到下一個毫秒,直到獲得新的時間戳
        /// </summary>
        /// <param name="lastTimestamp">上次生成ID的時間截</param>
        /// <returns>當前時間戳</returns>
        private static long GetNextTimestamp(long lastTimestamp)
        {
            long timestamp = GetCurrentTimestamp();
            while (timestamp <= lastTimestamp)
            {
                timestamp = GetCurrentTimestamp();
            }
            return timestamp;
        }

        /// <summary>
        /// 獲取當前時間戳
        /// </summary>
        /// <returns></returns>
        private static long GetCurrentTimestamp()
        {
            return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds;
        }

        private static readonly DateTime Jan1st1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    }
}
View Code

分布式訂單擴展類,以便Strarup中可以注入到容器:DistributedOrderSnServiceCollectionExtensions

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;

namespace RuanMou.Projects.Commons.Distributes
{
    /// <summary>
    /// ServiceCollection 分布式訂單號擴展
    /// </summary>
    public static class DistributedOrderSnServiceCollectionExtensions
    {
        /// <summary>
        ///  注冊分布式Redis集群緩存
        /// </summary>
        /// <typeparam name="connectionString"></typeparam>
        /// <returns></returns>
        public static IServiceCollection AddDistributedOrderSn(this IServiceCollection services, long datacenterId, long workerId)
        {
            // 1、注冊雪花Id
            SnowflakeId snowflakeId = new SnowflakeId(datacenterId, workerId);
            services.AddSingleton(snowflakeId);

            // 2、注冊分布式訂單號
            services.AddSingleton<DistributedOrderSn>();
            return services;
        }
    }
}
View Code

在聚合服務的Straup中注入:

// 10、添加分布式訂單
services.AddDistributedOrderSn(1, 1);

 

秒殺聚合服務訂單控制器類的最終代碼:

using AutoMapper;
using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using RuanMou.Projects.Commons.Distributes;
using RuanMou.Projects.Commons.Exceptions;
using RuanMou.Projects.Commons.Users;
using RuanMou.Projects.Commons.Utils;
using RuanMou.Projects.OrderServices.Models;
using RuanMou.Projects.SeckillAggregateServices.Caches.SeckillStock;
using RuanMou.Projects.SeckillAggregateServices.Dto.PaymentService;
using RuanMou.Projects.SeckillAggregateServices.Dto.ProductService;
using RuanMou.Projects.SeckillAggregateServices.Forms.OrderService;
using RuanMou.Projects.SeckillAggregateServices.Models;
using RuanMou.Projects.SeckillAggregateServices.Models.OrderService;
using RuanMou.Projects.SeckillAggregateServices.Models.SeckillService;
using RuanMou.Projects.SeckillAggregateServices.Services;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace RuanMou.Projects.SeckillAggregateServices.Controllers
{
    /// <summary>
    /// 訂單聚合控制器
    /// 從粗取精  去偽存真  由此及彼  由表及里 
    /// while
    /// 抽象目的 幫助我們形成現象 20個屬性 18 ------ 概念 class 
    /// </summary>
    [Route("api/Order")]
    [ApiController]
    public class OrderController : ControllerBase
    {
        private readonly IOrderClient OrderClient;
        private readonly ISeckillsClient seckillsClient;
        private readonly IMemoryCache memoryCache;

        private readonly ISeckillStockCache seckillStockCache;
        private readonly ICapPublisher capPublisher;
        private readonly DistributedOrderSn distributedOrderSn;
        public OrderController(IOrderClient orderClient,
                               ISeckillsClient seckillsClient,
                               IMemoryCache memoryCache,
                               ISeckillStockCache seckillStockCache,
                               ICapPublisher capPublisher,
                               DistributedOrderSn distributedOrderSn)
        {
            this.OrderClient = orderClient;
            this.seckillsClient = seckillsClient;
            this.memoryCache = memoryCache;
            this.seckillStockCache = seckillStockCache;
            this.capPublisher = capPublisher;
            this.distributedOrderSn = distributedOrderSn;
        }

        /// 創建預訂單
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet("{id}")]
        public OrderDto CreatePreOrder(SysUser sysUser, [FromForm] ProdcutPo prodcutPo)
        {
            // 1、創建訂單號
            string orderSn = OrderUtil.GetOrderCode();

            // 2、計算總價
            decimal ItemTotalPrice = prodcutPo.ProductCount * prodcutPo.ProductPrice;

            // 3、創建訂單項
            OrderItemDto orderItemDto = new OrderItemDto();
            orderItemDto.OrderSn = orderSn;
            orderItemDto.ProductId = prodcutPo.ProductId;
            orderItemDto.ItemPrice = prodcutPo.ProductPrice;
            orderItemDto.ItemCount = prodcutPo.ProductCount;
            orderItemDto.ItemTotalPrice = ItemTotalPrice;

            // 4、創建訂單項
            OrderDto orderDto = new OrderDto();
            orderDto.UserId = sysUser.UserId;

            orderDto.OrderItemDtos = new List<OrderItemDto>() {
                orderItemDto
            };
            return orderDto;
        }

        /// <summary>
        /// 3.1 發送創建訂單消息
        /// </summary>
        /// <param name="ProductId"></param>
        /// <param name="ProductCount"></param>
        private void SendOrderCreateMessage(int userId, string orderSn, OrderPo orderPo)
        {
            var configuration = new MapperConfiguration(cfg =>
            {
                cfg.CreateMap<OrderPo, Order>();
            });

            IMapper mapper = configuration.CreateMapper();

            // 2、設置訂單
            Order order = mapper.Map<OrderPo, Order>(orderPo);
            order.OrderSn = orderSn;
            order.OrderType = "1";// 訂單類型(1、為秒殺訂單)
            order.UserId = userId;

            // 3、設置訂單項
            OrderItem orderItem = new OrderItem();
            orderItem.ItemCount = orderPo.ProductCount;
            orderItem.ItemPrice = orderPo.OrderTotalPrice;
            orderItem.ItemTotalPrice = orderPo.OrderTotalPrice;
            orderItem.ProductUrl = orderPo.ProductUrl;
            orderItem.ProductId = orderPo.ProductId;
            orderItem.OrderSn = orderSn;

            List<OrderItem> orderItems = new List<OrderItem>();
            orderItems.Add(orderItem);
            order.OrderItems = orderItems;

            // 4、發送訂單消息
            capPublisher.Publish<Order>("seckill.order", order);
        }

        /// <summary>
        /// 4.6、創建訂單(redis + 消息隊列 + lua + 方法冪等 + 失敗回滾 + 分布式訂單號)
        /// </summary>
        /// <param name="orderDto"></param>
        [HttpPost]
        public PaymentDto CreateOrder(SysUser sysUser, [FromForm] OrderPo orderPo)
        {
            // 1、秒殺參數准備
            string ProductKey = Convert.ToString(orderPo.ProductId);// 商品key
            string SeckillLimitKey = "seckill_stock_:SeckillLimit" + orderPo.ProductCount; // 單品限流key
            string UserBuyLimitKey = "seckill_stock_:UserId" + sysUser.UserId + "ProductId" + orderPo.ProductId;// 用戶購買限制key
            int productCount = orderPo.ProductCount; // 購買商品數量
            int requestCountLimits = 60000; // 單品限流數量
            int seckillLimitKeyExpire = 60;// 單品限流時間:單位秒
            string requestIdKey = "seckill_stock_:" + orderPo.RequestId; // requestIdKey
            string orderSn = distributedOrderSn.CreateDistributedOrderSn(); // 分布式訂單號 "97006545732243456"

            // 2、執行秒殺
            var SeckillResult = RedisHelper.EvalSHA(memoryCache.Get<string>("luaSha"), ProductKey, UserBuyLimitKey, SeckillLimitKey, productCount, requestCountLimits, seckillLimitKeyExpire, requestIdKey, orderSn);
            if (!SeckillResult.ToString().Equals("1"))
            {
                throw new BizException(SeckillResult.ToString());
            }
            //秒殺失敗回滾
            try
            {
                // 3、發送訂單消息到rabbitmq
                SendOrderCreateMessage(sysUser.UserId, orderSn, orderPo);
            }
            catch (Exception)
            {
                // 3.1 秒殺回滾
                RedisHelper.EvalSHA(memoryCache.Get<string>("luaShaCallback"), ProductKey, UserBuyLimitKey, productCount, requestIdKey, orderSn);

                // 3.2 搶購失敗
                throw new BizException("搶購失敗");
            }

            // 4、創建支付信息
            PaymentDto paymentDto = new PaymentDto();
            paymentDto.OrderSn = orderSn;
            paymentDto.OrderTotalPrice = orderPo.OrderTotalPrice;
            paymentDto.UserId = sysUser.UserId;
            paymentDto.ProductId = orderPo.ProductId;
            paymentDto.ProductName = orderPo.ProductName;

            return paymentDto;
        }
    }
}
View Code

最后優化完畢后壓測結果

單台服務每秒並發數:3000每秒

3台集群服務器峰值:2.5萬

20萬秒殺所需服務器

  • 1秒內秒殺完:
  • 10秒內秒殺完:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM