基于redis设计的秒杀活动
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设计的秒杀活动的更多相关文章
- java基于redis事务的秒杀实现
package com.vian.user.service; import org.junit.Test; import org.springframework.util.CollectionUtil ...
- 动手造轮子:基于 Redis 实现 EventBus
动手造轮子:基于 Redis 实现 EventBus Intro 上次我们造了一个简单的基于内存的 EventBus,但是如果要跨系统的话就不合适了,所以有了这篇基于 Redis 的 EventBus ...
- 使用Redis中间件解决商品秒杀活动中出现的超卖问题(使用Java多线程模拟高并发环境)
一.引入Jedis依赖 可以新建Spring或Maven工程,在pom文件中引入Jedis依赖: <dependency> <groupId>redis.clients< ...
- laravel基于redis实现的一个简单的秒杀系统
说明:网上很多redis秒杀系统的文章,看的都是一头雾水,然后自己来实现一个,也方便以后自己学习 实现的方式是用的redis的list队列,框架为laravel 核心部分为list的pop操作,此操作 ...
- 【总结】瞬时高并发(秒杀/活动)Redis方案(转)
转载地址:http://bradyzhu.iteye.com/blog/2270698 1,Redis 丰富的数据结构(Data Structures) 字符串(String) Redis字符串能包含 ...
- 【总结】瞬时高并发(秒杀/活动)Redis方案
1,Redis 丰富的数据结构(Data Structures) 字符串(String) Redis字符串能包含任意类型的数据 一个字符串类型的值最多能存储512M字节的内容 利用INCR命令簇(IN ...
- 解决秒杀活动高并发出现负库存(Redis)
商城在秒杀活动开始时,同时有好多人来请求这个接口,即便做了判断库存逻辑,也难免防止库存出现超卖,造成损失 Django中的ORM本身就对数据库做了防范,但再过亿级访问也扛不住 下面利用Redis的过载 ...
- 基于Redis的限流系统的设计
本文讲述基于Redis的限流系统的设计,主要会谈及限流系统中限流策略这个功能的设计:在实现方面,算法使用的是令牌桶算法来,访问Redis使用lua脚本. 1.概念 In computer netw ...
- 基于Redis的分布式锁设计
前言 基于Redis的分布式锁实现,原理很简单嘛:检测一下Key是否存在,不存在则Set Key,加锁成功,存在则加锁失败.对吗?这么简单吗? 如果你真这么想,那么你真的需要好好听我讲一下了.接下来, ...
- 基于Redis&MySQL接口幂等性设计
基于Redis&MySQL接口幂等性设计 欲把相思说似谁,浅情人不知. 1.幂等 幂等性即多次调用接口或方法不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致. 2.幂等使用场景 前 ...
随机推荐
- vue打包后打开index.html文件显示空白页问题
通过网上的资料发现在vue.config.js中写入再重新打包就可以再index.html中显示. https://blog.csdn.net/m0_51060602/article/details/ ...
- 【情景题】NPDP经典题目(上)
[情景题]NPDP经典题目(上) 1.一家玩具生产企业正在为10-12岁的儿童开发一种"动力车".潜在风险:尽管该公司在玩具市场有着丰富的经验,但是这些经验主要是针对5岁以下儿童玩 ...
- nohup文件的压缩分割
编写sh脚本 先拷贝,之后,清空. 待完成,压缩功能 #!/bin/sh #description split logs time1=$(date -d 'yesterday' "+%Y%m ...
- python 查找文件夹下以特定字符开头的某类型文件 - os.walk
Python os.walk() 方法 os.walk() 方法用于通过在目录树中游走输出在目录中的文件名,向上或者向下.os.walk() 方法是一个简单易用的文件.目录遍历器,可以帮助我们高效的处 ...
- linux 网络操作 route iptables ufw
linux 网络操作 route iptables ufw sudo ufw status sudo ufw allow ssh sudo ufw allow http sudo ufw deny h ...
- 在VSCODE的终端运行Python时汉字乱码问题处理
问题描述 在VSCODE的终端运行Python时,打印输出中文时汉字出现乱码, 文件编码都是UTF-8 解决步骤 1.打开Settings配置窗口(Ctrl+,) 2.搜索:code-runner.e ...
- unity animation instance
animation instance piti6/UnityGpuInstancedAnimation https://github.com/piti6/UnityGpuInstancedAnimat ...
- adobe 色轮
https://color.adobe.com/zh/create/color-wheel
- linux发展史及软件配置
linux岗位需求 # 1.岗位需求 自动化运维,容器运维,DBA,IDC运维(不建议) ps:linux岗位会的越多给的越多 linux工作本质 linux简要发展史 # 1.发展 1991年,芬兰 ...
- 访问网络共享(net use):发生系统错误 67。找不到网络名。
使用\\ip访问对方共享目录或使用net use \\ip 时: 发生系统错误 67.找不到网络名. 以下几项启用: 1,网卡勾选"Microsoft网络客户端". 2,启用服务& ...