FlashSale 意为 秒杀,是电子网上商城促销活动的一种形式

本项目依赖redis,使用redis的缓存以及原子操作实现秒杀活动

依赖的包

StackExchange.Redis  该包的作用类似redis client,可以实现原生操作 
 Microsoft.Extensions.Caching.StackExchangeRedis  该包的作用偏向缓存用途,用来添加缓存、删除缓存

秒杀活动的设计

前端设计

将流量在上游系统中拦截

比如浏览器中 限时5秒只能请求一次
然后按钮置灰 防止用户重复点

更极端的,可以在前端生成0-1之间的随机数,随机数大于等于0.9,则发送真正的http请求,小于0.9,直接提示用户抢购/秒杀失败

后端设计

后台防止黑客,对接口限流,也是5秒 每个用户只能请求一次

秒杀是一个读多写少的场景、因此可以用缓存来扛高并发的读,防止流量到达数据库

设计秒杀活动的表结构

| 字段 |字段的描述 |
| :------------: | :------------: |
| Id | 秒杀活动的Id |
| Name | 秒杀活动的名称 宣传语 |
| ProductId |要秒杀的商品Id |
| ProductCount | 本次秒杀活动计划售出商品的数量 必须大于等于1 |
| EachUserCanBuy|每个参与活动的用户最多能抢购的数量 大于等于1 且小于等于 ProductCount|
| StartAt | 活动开始的时间 活动开始时间必须大于当前时间 + 10分钟,也就是最快只能10分钟后才开始 |
| EndAt | 活动结束的时间,结束时间必须大于等于 (开始时间 + 5分钟),即每场秒杀活动最短可以持续5分钟|

public class FlashSale
{
public int Id { get; set; } /// <summary>
/// 秒杀活动的名称
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// 要秒杀的商品Id
/// </summary> public int ProductId { get; set; }
/// <summary>
/// 本次秒杀活动计划售出商品的数量 必须大于等于1
/// </summary>
public int ProductCount { get; set; }
/// <summary>
/// 每个参与活动的用户最多能抢购的数量 大于等于1 且小于等于 ProductCount
/// </summary>
public int EachUserCanBuy { get; set; }
/// <summary>
/// 活动开始的时间
/// </summary>
public DateTimeOffset StartAt { get; set; }
/// <summary>
/// 活动结束的时间
/// </summary>
public DateTimeOffset EndAt { get; set; }
}

  

创建秒杀活动的时候,需要提交上述信息,
然后 秒杀开始前5分钟,不可以编辑秒杀活动了

更新本次秒杀活动需要删除缓存(做最终一致性)
对Microsoft.Extensions.Caching.StackExchangeRedis的IDistributedCache进行扩展

using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed; namespace FlashSale.Extensions
{
/// <summary>
/// 本扩展是对Microsoft.Extensions.Caching.StackExchangeRedis包
/// 中一些方法的扩展
/// 注意:本方法仅仅是扩展,如果要做缓存与数据库数据最终一致性,用锁防止流量打到数据的操作,请在各自的service中做
/// </summary>
public static class DistributedCacheExtensions
{
/// <summary>
/// 该方法是对IDistributedCache中setStringAsync的扩展
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cache">IDistributedCache</param>
/// <param name="recordKey">缓存的key</param>
/// <param name="record">缓存的value</param>
/// <param name="absoluteExpirationRelativeToNow">过期时间,可以为null,当为null的时候默认
/// 添加5分钟 + 2分钟内随机的时间。即 过期时间大于等于5分钟小于等于7分钟</param>
/// <param name="slidingExpireTIme">滑动过期时间 可以为null. 注意SlidingExpiration指的是 在这段时间 如果该key没有被访问,则会被删除</param>
/// <returns></returns>
public static async Task SetRecordAsync<T>(
this IDistributedCache cache,
string recordKey,
T record,
TimeSpan? absoluteExpirationRelativeToNow = null,
TimeSpan? slidingExpireTIme = null
)
{
var cacheOptions = new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow ?? TimeSpan.FromSeconds(5 * 60 + new Random().Next(1, 2 * 60)),
SlidingExpiration = slidingExpireTIme
};
var jsonData = JsonSerializer.Serialize(record);
await cache.SetStringAsync(recordKey, jsonData, cacheOptions);
}
/// <summary>
/// 通过缓存的key查找缓存的内容
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cache">IDistributedCache</param>
/// <param name="recordKey">缓存的key</param>
/// <returns></returns>
public static async Task<T?> GetRecordAsync<T>(this IDistributedCache cache, string recordKey)
{
var jsonData = await cache.GetStringAsync(recordKey);
return jsonData is null ? default(T) : JsonSerializer.Deserialize<T>(jsonData);
}
}
}

  

配合redis 需要三个key

第一个key,用来缓存上面的活动。 本次key的名称是 flashSale:活动id
第二个key,用来做商品数量的计数器 防止超卖(秒杀活动创建成功后 这个key也随之创建) 这个key 既可以incr也可以decr,如果是decr,则需要设置key初始值等于秒杀商品的数量 . 本次测试的key 名称是flashSale:活动Id:商品Id
第三个key 用来记录某个用户抢成功的次数(这个主要是配合 一个用户最多可以抢几个这个功能)

注意的地方:就是秒杀活动不预占库存,客户需对库存自行把握

业务:
将活动缓存起来,前端用户就是刷新而已

当请求进来的时候,判断当前时间点是否处于活动期间

否返回badrequest

使用incr 原子操作,对该用户参加这次活动的key 做+1 操作,如果结果大于 活动中设置的每个用户可以抢购的最大个数,则返回bad request

然后对本次活动 的第二个key 商品的key做incr操作
如果结果大于商品的数量,说明商品被抢光了,直接返回即可

 1 using FlashSale.Extensions;
2 using FlashSale.Interfaces;
3 using Microsoft.Extensions.Caching.Distributed;
4 using StackExchange.Redis;
5 namespace FlashSale.ImplementServices
6 {
7 public class FlashSaleService : IFlashSaleService
8 {
9 private readonly IDistributedCache _distributedCache;
10 private readonly IDatabase _redisDatabase;
11
12 public FlashSaleService(IDistributedCache distributedCache,
13 IConnectionMultiplexer connectionMultiplexer)
14 {
15 _distributedCache = distributedCache;
16 _redisDatabase = connectionMultiplexer.GetDatabase(); // 本次设计:缓存和秒杀的业务逻辑用同一个数据库,即第0个redis数据库
17 }
18
19 /// <inheritdoc />
20 public async Task<Models.FlashSale?> GetFlashSaleAsync(string flashSaleId)
21 {
22 // 注意:在生产环境中,会发生缓存失效而需要去读取数据库的情况,此时,会发生大量读的请求的流量
23 // 为了不让这么多读的请求流量打到数据库,我们需要加锁,只有获取到锁的请求,才有资格去数据库读取数据
24 // 读取到数据之后,把数据刷入缓存,那些获取不到锁的用户直接返回当前请求的用户过多,请稍后重试
25 // 数据刷入缓存后,用户会刷新页面,此时从缓存中读取数据即可
26 return await _distributedCache.GetRecordAsync<Models.FlashSale>($"flashSale:{flashSaleId}");
27 }
28
29 /// <inheritdoc />
30 public async Task Execute(string flashSaleId)
31 {
32
33 var flashSale = await _distributedCache.GetRecordAsync<Models.FlashSale>($"flashSale:{flashSaleId}");
34 if (flashSale is null)
35 {
36 return;
37 }
38
39 var dateTimeNow = DateTimeOffset.Now;
40 if ( dateTimeNow < flashSale.StartAt)
41 {
42 Console.WriteLine("活动还没有开始");
43 return;
44 }
45
46 if (dateTimeNow > flashSale.EndAt)
47 {
48 Console.WriteLine("活动已经结束了");
49 return;
50 }
51
52 // 要进入秒杀活动的逻辑环节
53 // 这个key 初始化的时候会设置为0 incr操作是原子性
54 if (await _redisDatabase.StringIncrementAsync($"flashSale:{ flashSaleId }:{ flashSale.ProductId}") > flashSale.ProductCount)
55 {
56 // 没抢到
57 Console.WriteLine("抢光了");
58
59 }
60 else
61 {
62 // 抢到了
63 Console.WriteLine("恭喜您,抢到了");
64 }
65
66 }
67 }
68 }

Apache BenchMark测试

机器配置:4核心 32G内存

分配给Docker的资源是2核心 6G内存

请求数1000和并发数100的测试

上面的数据表现不是很好,于是我换了另一台机器,表现如下:

数据解读(第一台机器):

从上到下,可以发现有2个Time per request的指标:

第一个Time per request = 第二个Time per request * Concurrency Level, 因此 29.7361ms乘以并发数100等于2973.610ms。

第二个Time per request = Time taken for tests * 1000 / 100,即29.736秒乘以1000除以100等于29.736毫秒。因此Request per second是1000 / 29.736大约等于33.63,即每秒钟处理33.63个请求。

请求数1000和并发数500的测试结果:

请求数100000和并发数10000的测试结果

可以看到每秒钟处理40.34个请求,百分之50的请求的响应时间位于258022ms以内,大约是4.3分钟,响应时间最长的是272570ms,大约4.5分钟。

这里已经测出来机器的最高处理能力了,即每秒处理40.34个请求,想要再提高处理能力,可以往水平扩展方向寻求思路。

整个测试下来,个人觉发现一个问题:就是测试的同时,自己手动(postman或者swagger)调用api,响应的时间是160ms左右,和apache benchmark给出的结果相去甚远,

我认为应该是机器的问题,老机器服役7年了,于是换了一台机器,测试请求数10w,并发数200的情况如下:

换了机器之后,表现就好很多,时间最长的请求耗时4502ms,约等于4.5s,百分之50的请求消耗的时间均在491ms以内。

让我们来看一下redis-benchmark的结果:

处理抢购成功的业务逻辑

接下来 对于抢购到的用户,写一个消息 放到消息队列,然后业务提示用户抢购到了,请到订单中支付

前端可以根据本次活动id 和商品id 查询第二个key,用来作为页面秒杀入口是否置灰色的一个判断依据。 如果key大于本次活动的商品数量,则显示已抢光。

收尾工作:秒杀活动中,商品可能存在剩余,就是用户没有把商品抢光,则需要等到活动结束后,人为手动 点一下 把商品的库存还回去,之后删除第二个key和第三个key 第一个key有过期时间 过期了自动删除

用户抢到商品了,但是没有支付,
1 用户点击取消订单,则第二key decr ,相当于把库存还回去 (这里做不到,因为这个key可能被其他用户incr超过本次活动售卖的数量,所以还回去的话不太现实,这里需要想想其他的办法 看看能不能实现)

2用户在订单超过付款时间也没付款,则用定时任务把库存还回去。还回去有两种,判断活动是否还在进,进行的话 则和1的操作一样,活动过期了,则返回到真实仓库库存 注意:订单需要有最迟付款时间字段(不为空),以及真实付款时间字段(可为空)

后记

如果文中有字词错误,欢迎指出。对于技术实现有不同的看法或者改进,也欢迎指出。共同学习和进步。

基于redis设计的秒杀活动的更多相关文章

  1. java基于redis事务的秒杀实现

    package com.vian.user.service; import org.junit.Test; import org.springframework.util.CollectionUtil ...

  2. 动手造轮子:基于 Redis 实现 EventBus

    动手造轮子:基于 Redis 实现 EventBus Intro 上次我们造了一个简单的基于内存的 EventBus,但是如果要跨系统的话就不合适了,所以有了这篇基于 Redis 的 EventBus ...

  3. 使用Redis中间件解决商品秒杀活动中出现的超卖问题(使用Java多线程模拟高并发环境)

    一.引入Jedis依赖 可以新建Spring或Maven工程,在pom文件中引入Jedis依赖: <dependency> <groupId>redis.clients< ...

  4. laravel基于redis实现的一个简单的秒杀系统

    说明:网上很多redis秒杀系统的文章,看的都是一头雾水,然后自己来实现一个,也方便以后自己学习 实现的方式是用的redis的list队列,框架为laravel 核心部分为list的pop操作,此操作 ...

  5. 【总结】瞬时高并发(秒杀/活动)Redis方案(转)

    转载地址:http://bradyzhu.iteye.com/blog/2270698 1,Redis 丰富的数据结构(Data Structures) 字符串(String) Redis字符串能包含 ...

  6. 【总结】瞬时高并发(秒杀/活动)Redis方案

    1,Redis 丰富的数据结构(Data Structures) 字符串(String) Redis字符串能包含任意类型的数据 一个字符串类型的值最多能存储512M字节的内容 利用INCR命令簇(IN ...

  7. 解决秒杀活动高并发出现负库存(Redis)

    商城在秒杀活动开始时,同时有好多人来请求这个接口,即便做了判断库存逻辑,也难免防止库存出现超卖,造成损失 Django中的ORM本身就对数据库做了防范,但再过亿级访问也扛不住 下面利用Redis的过载 ...

  8. 基于Redis的限流系统的设计

    本文讲述基于Redis的限流系统的设计,主要会谈及限流系统中限流策略这个功能的设计:在实现方面,算法使用的是令牌桶算法来,访问Redis使用lua脚本.   1.概念 In computer netw ...

  9. 基于Redis的分布式锁设计

    前言 基于Redis的分布式锁实现,原理很简单嘛:检测一下Key是否存在,不存在则Set Key,加锁成功,存在则加锁失败.对吗?这么简单吗? 如果你真这么想,那么你真的需要好好听我讲一下了.接下来, ...

  10. 基于Redis&MySQL接口幂等性设计

    基于Redis&MySQL接口幂等性设计 欲把相思说似谁,浅情人不知. 1.幂等 幂等性即多次调用接口或方法不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致. 2.幂等使用场景 前 ...

随机推荐

  1. vue打包后打开index.html文件显示空白页问题

    通过网上的资料发现在vue.config.js中写入再重新打包就可以再index.html中显示. https://blog.csdn.net/m0_51060602/article/details/ ...

  2. 【情景题】NPDP经典题目(上)

    [情景题]NPDP经典题目(上) 1.一家玩具生产企业正在为10-12岁的儿童开发一种"动力车".潜在风险:尽管该公司在玩具市场有着丰富的经验,但是这些经验主要是针对5岁以下儿童玩 ...

  3. nohup文件的压缩分割

    编写sh脚本 先拷贝,之后,清空. 待完成,压缩功能 #!/bin/sh #description split logs time1=$(date -d 'yesterday' "+%Y%m ...

  4. python 查找文件夹下以特定字符开头的某类型文件 - os.walk

    Python os.walk() 方法 os.walk() 方法用于通过在目录树中游走输出在目录中的文件名,向上或者向下.os.walk() 方法是一个简单易用的文件.目录遍历器,可以帮助我们高效的处 ...

  5. linux 网络操作 route iptables ufw

    linux 网络操作 route iptables ufw sudo ufw status sudo ufw allow ssh sudo ufw allow http sudo ufw deny h ...

  6. 在VSCODE的终端运行Python时汉字乱码问题处理

    问题描述 在VSCODE的终端运行Python时,打印输出中文时汉字出现乱码, 文件编码都是UTF-8 解决步骤 1.打开Settings配置窗口(Ctrl+,) 2.搜索:code-runner.e ...

  7. unity animation instance

    animation instance piti6/UnityGpuInstancedAnimation https://github.com/piti6/UnityGpuInstancedAnimat ...

  8. adobe 色轮

    https://color.adobe.com/zh/create/color-wheel

  9. linux发展史及软件配置

    linux岗位需求 # 1.岗位需求 自动化运维,容器运维,DBA,IDC运维(不建议) ps:linux岗位会的越多给的越多 linux工作本质 linux简要发展史 # 1.发展 1991年,芬兰 ...

  10. 访问网络共享(net use):发生系统错误 67。找不到网络名。

    使用\\ip访问对方共享目录或使用net use \\ip 时: 发生系统错误 67.找不到网络名. 以下几项启用: 1,网卡勾选"Microsoft网络客户端". 2,启用服务& ...