电商秒杀系统:服务器集群、分布式缓存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秒内秒杀完:
原文地址:https://www.cnblogs.com/qingyunye/p/13779624.html