本篇将和大家分享一下秒杀商品活动架构,采用的架构方案正如标题名称.NetCore+Jexus代理+Redis,由于精力有限所以这里只设计到商品添加,抢购,订单查询,处理队列抢购订单的功能;有不足或者不够详细的还请见谅,顺手点个推荐也不错;
a. 秒杀流程
b. 封装
StackExchange.Redis
的使用类c. Ubuntu16.04上使用
Jexus
搭建代理完成分布式部署d.
NetCore
写实时监控队列服务
a. 该项目git开源地址: https://github.com/shenniubuxing3/SeckillPro ,线上效果地址: http://www.lovexins.com:3333/
b.
SeckillPro.Web
:面向用户的web站点,主要提供商品展示,秒杀抢购,抢购结果,订单列表等功能;c.
SeckillPro.Api
:主要处理秒杀活动的请求,然后加入到秒杀队列中,以及订单状态的查询接口;d.
SeckillPro.Server
:处理秒杀队列的服务;根据Redis
模糊匹配key的方式,开启多个商品秒杀的任务,并处理秒杀请求和改变订单抢购状态;e.
SeckillPro.Com
:集成公共的方法;这里面前有操作Redis
的list,hash,string的封装类;
SeckillPro.Web
商品后台管理对于商品活动来说,商品维护是必不可少的,由于这里商品维护的信息比较少,并且这里只加入到了RedisDb
中,所以就不直接上代码了;一个列表,一个添加仅此而已;这里就不再贴代码了,如果你感兴趣可以去我的git上面看源码: https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Web/Controllers/HomeController.cs
SeckillPro.Web
用户端商品列表+秒杀请求+用户订单列表
商品列表和订单列表没有可以太多说的,一般订单系统都有这两个列表;关键点在于订单秒杀流程中,咋们来简单分析下面向客户秒杀的流程需要注意的事项:
a. 限制秒杀开始时间和结束时间(测试未限制)
b. 未开始活动限制提交按钮不可点(测试未限制)
c. 获取真实剩余库存限制秒杀提交(获取redis中商品hash存储的真实剩余量)
d. 把客户的秒杀请求转移到另外的api集群,以此提高面向客户端的web站点并发承载率(测试项目中我直接指定4545端口的api测试)
这里就不再贴代码了,如果你感兴趣可以去我的git上面看看这部分源码: https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Web/Controllers/HomeController.cs
这个处理队列服务处理流程:模糊匹配Redis
中每种商品的队列key-》开启不同商品的处理队列任务-》处理秒杀订单-》更新库存和秒杀订单状态;
a. 模糊匹配Redis
中每种商品的队列key
:这里采用的是StackExchange.Redis
中指定redis
原生命令的方法来获取匹配队列key
,设计的代码如下:
///
/// 模糊匹配redis中的key
///
///
///
public async Taskstring>> MatchKeys(params string[] paramArr)
{
var list = new List<string>();
try
{
var result = await this.ExecuteAsync("keys", paramArr);
var valArr = ((RedisValue[])result);
foreach (var item in valArr)
{
list.Add(item);
}
}
catch (Exception ex) { }
return list;
}
///
/// 执行redis原生命令
///
///
///
///
public async Task ExecuteAsync(string cmd, params string[] paramArr)
{
try
{
var db = this.GetDb();
return await db.ExecuteAsync(cmd, paramArr);
}
catch (Exception ex) { }
return default(RedisResult);
}
b. 开启不同商品的处理队列任务:通过Task.Factory.StartNew(action,object)方法开启不同商品的处理秒杀订单的任务;
c. 更新库存和秒杀订单状态:由于抢购商品要求库存剩余实时性,所以每处理一个抢购订单,需要对该商品减去相应的库存和修改秒杀订单的状态方便用户查看秒杀结果;
d. 处理队列具体的实现代码可以去git看下,个人觉得还是有用的:https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Server/Program.cs
这里部署的代理采用的是Jexus
代理;作为在linux
和unix
上部署.net程序实用的工具,真的很感谢jexus
作者;首先本篇讲解的部署环境是ubunt16.04x64(至于这么安装jexus可以参考上一篇分享文章),为了更直观的看出来效果我在服务器上拷贝了两份SeckillPro.Web
发布的站点,他们代码都是一样的只是分别把_Layout.cshtml
试图模板中加入了端口7777
和8888
,我就用这两个端口来测试jexus的代理效果;
测试方便直接分别在两个复制站点中执行如下终端命令:dotnet SeckillPro.Web.dll http://ip:端口
;一个监听7777端口一个监听8888;执行命令效果图:
监听7777和8888端口成功后,我们就可以直接在浏览器输入:http://172.16.9.66:7777 访问,正常情况下能够看到如下图示例:
单个站点访问没问题了,下面开始配置jexus代理;只需要在jexus/siteconf的配置文件中(我这里是default配置文件),增加如下设置:
注意reproxy参数:
a. 第一个/表示根目录,一般不变
b. 多个被代理地址使用‘,’隔开;
c. 被代理地址后面也同样需要加/
此时我们配置完后,只需要启动jexus
就行了:./jws start
(怎么启动可以参考上一篇文章);当启动jws成功后,我们就能通过配置的80端口,来访问SeckillPro.Web
站点了,效果图:
至于代理分发的策略暂不在本章的讨论范围内,如果可以建议去jexus官网了解下;同样对于Seckill.Api
我们也可以这样部署,这里部署了个秒杀线上地址,有兴趣的朋友可以点击试试:http://www.lovexins.com:3333/ (注:这里没有使用代理)
StackExchange.Redis
的使用类StackRedis.cs
其实这个在之前已经分享过了,只不过只有操作string
和list
的分装;本篇测试涉及到订单查询和商品查询等功能,所以这里我又扩展了对hash
的操作方法,可以说更丰富了吧,如果您正打算使用redis
或许直接用我这个封装类是个不错的打算;
public class StackRedis : IDisposable
{
#region 配置属性 基于 StackExchange.Redis 封装
//连接串 (注:IP:端口,属性=,属性=)
public string _ConnectionString = "127.0.0.1:6377,password=shenniubuxing3";
//操作的库(注:默认0库)
public int _Db = 0;
#endregion
#region 管理器对象
///
/// 获取redis操作类对象
///
private static StackRedis _StackRedis;
private static object _locker_StackRedis = new object();
public static StackRedis Current
{
get
{
if (_StackRedis == null)
{
lock (_locker_StackRedis)
{
_StackRedis = _StackRedis ?? new StackRedis();
return _StackRedis;
}
}
return _StackRedis;
}
}
///
/// 获取并发链接管理器对象
///
private static ConnectionMultiplexer _redis;
private static object _locker = new object();
public ConnectionMultiplexer Manager
{
get
{
if (_redis == null)
{
lock (_locker)
{
_redis = _redis ?? GetManager(this._ConnectionString);
return _redis;
}
}
return _redis;
}
}
///
/// 获取链接管理器
///
///
///
public ConnectionMultiplexer GetManager(string connectionString)
{
return ConnectionMultiplexer.Connect(connectionString);
}
///
/// 获取操作数据库对象
///
///
public IDatabase GetDb()
{
return Manager.GetDatabase(_Db);
}
#endregion
#region 操作方法
#region string 操作
///
/// 根据Key移除
///
///
///
public async Task<bool> Remove(string key)
{
var db = this.GetDb();
return await db.KeyDeleteAsync(key);
}
///
/// 根据key获取string结果
///
///
///
public async Task<string> Get(string key)
{
var db = this.GetDb();
return await db.StringGetAsync(key);
}
///
/// 根据key获取string中的对象
///
///
///
///
public async Task Get(string key)
{
var t = default(T);
try
{
var _str = await this.Get(key);
if (string.IsNullOrWhiteSpace(_str)) { return t; }
t = JsonConvert.DeserializeObject(_str);
}
catch (Exception ex) { }
return t;
}
///
/// 存储string数据
///
///
///
///
///
public async Task<bool> Set(string key, string value, int expireMinutes = 0)
{
var db = this.GetDb();
if (expireMinutes > 0)
{
return db.StringSet(key, value, TimeSpan.FromMinutes(expireMinutes));
}
return await db.StringSetAsync(key, value);
}
///
/// 存储对象数据到string
///
///
///
///
///
///
public async Task<bool> Set(string key, T value, int expireMinutes = 0)
{
try
{
var jsonOption = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
var _str = JsonConvert.SerializeObject(value, jsonOption);
if (string.IsNullOrWhiteSpace(_str)) { return false; }
return await this.Set(key, _str, expireMinutes);
}
catch (Exception ex) { }
return false;
}
///
/// 是否存在key
///
///
///
///
public async Task<bool> KeyExists(string key)
{
try
{
var db = this.GetDb();
return await db.KeyExistsAsync(key);
}
catch (Exception ex) { }
return false;
}
#endregion
#region hash操作
///
/// 是否存在hash的列
///
///
///
///
public async Task<bool> HashFieldExists(string key, string filedKey)
{
try
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(filedKey)) { return false; }
var result = await this.HashFieldsExists(key, new Dictionary<string, bool> { { filedKey, false } });
return result[filedKey];
}
catch (Exception ex) { }
return false;
}
///
/// 是否存在hash的列集合
///
///
///
///
public async Taskstring, bool>> HashFieldsExists(string key, Dictionary<string, bool> dics)
{
try
{
if (dics.Count <= 0) { return dics; }
var db = this.GetDb();
foreach (var fieldKey in dics.Keys)
{
dics[fieldKey] = await db.HashExistsAsync(key, fieldKey);
}
}
catch (Exception ex) { }
return dics;
}
///
/// 设置hash
///
///
///
///
///
///
public async Task<long> SetOrUpdateHashsField(string key, string filedKey, T t, bool isAdd = true)
{
var result = 0L;
try
{
return await this.SetOrUpdateHashsFields(key, new Dictionary<string, T> { { filedKey, t } }, isAdd);
}
catch (Exception ex) { }
return result;
}
///
/// 设置hash集合,添加和更新操作
///
///
///
///
///
public async Task<long> SetOrUpdateHashsFields(string key, Dictionary<string, T> dics, bool isAdd = true)
{
var result = 0L;
try
{
var jsonOption = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
var db = this.GetDb();
foreach (var fieldKey in dics.Keys)
{
var item = dics[fieldKey];
var _str = JsonConvert.SerializeObject(item, jsonOption);
result += await db.HashSetAsync(key, fieldKey, _str) ? 1 : 0;
if (!isAdd) { result++; }
}
return result;
}
catch (Exception ex) { }
return result;
}
///
/// 移除hash的列
///
///
///
///
public async Task<bool> RemoveHashField(string key, string filedKey)
{
try
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(filedKey)) { return false; }
var result = await this.RemoveHashFields(key, new Dictionary<string, bool> { { filedKey, false } });
return result[filedKey];
}
catch (Exception ex) { }
return false;
}
///
/// 异常hash的列集合
///
///
///
///
public async Taskstring, bool>> RemoveHashFields(string key, Dictionary<string, bool> dics)
{
try
{
var jsonOption = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
var db = this.GetDb();
foreach (var fieldKey in dics.Keys)
{
dics[fieldKey] = await db.HashDeleteAsync(key, fieldKey);
}
return dics;
}
catch (Exception ex) { }
return dics;
}
///
/// 设置hash
///
///
///
///
///
///
public async Task GetHashField(string key, string filedKey)
{
var t = default(T);
try
{
var dics = await this.GetHashFields(key, new Dictionary<string, T> { { filedKey, t } });
return dics[filedKey];
}
catch (Exception ex) { }
return t;
}
///
/// 获取hash的列值集合
///
///
///
///
///
public async Taskstring, T>> GetHashFields(string key, Dictionary<string, T> dics)
{
try
{
var db = this.GetDb();
foreach (var fieldKey in dics.Keys)
{
var str = await db.HashGetAsync(key, fieldKey);
if (string.IsNullOrWhiteSpace(str)) { continue; }
dics[fieldKey] = JsonConvert.DeserializeObject(str);
}
return dics;
}
catch (Exception ex) { }
return dics;
}
///
/// 获取hash的key的所有列的值
///
///
///
///
public async Taskstring, T>> GetHashs(string key)
{
var dic = new Dictionary<string, T>();
try
{
var db = this.GetDb();
var hashFiles = await db.HashGetAllAsync(key);
foreach (var field in hashFiles)
{
dic[field.Name] = JsonConvert.DeserializeObject(field.Value);
}
return dic;
}
catch (Exception ex) { }
return dic;
}
///
/// 获取hash的Key的所有列的值的list集合
///
///
///
///
public async Task> GetHashsToList(string key)
{
var list = new List();
try
{
var db = this.GetDb();
var hashFiles = await db.HashGetAllAsync(key);
foreach (var field in hashFiles)
{
var item = JsonConvert.DeserializeObject(field.Value);
if (item == null) { continue; }
list.Add(item);
}
}
catch (Exception ex) { }
return list;
}
#endregion
#region List操作(注:可以当做队列使用)
///
/// list长度
///
///
///
///
public async Task<long> GetListLen(string key)
{
try
{
var db = this.GetDb();
return await db.ListLengthAsync(key);
}
catch (Exception ex) { }
return 0;
}
///
/// 获取List数据
///
///
///
///
public async Task> GetList(string key)
{
var t = new List();
try
{
var db = this.GetDb();
var _values = await db.ListRangeAsync(key);
foreach (var item in _values)
{
if (string.IsNullOrWhiteSpace(item)) { continue; }
t.Add(JsonConvert.DeserializeObject(item));
}
}
catch (Exception ex) { }
return t;
}
///
/// 获取队列出口数据并移除
///
///
///
///
public async Task GetListAndPop(string key)
{
var t = default(T);
try
{
var db = this.GetDb();
var _str = await db.ListRightPopAsync(key);
if (string.IsNullOrWhiteSpace(_str)) { return t; }
t = JsonConvert.DeserializeObject(_str);
}
catch (Exception ex) { }
return t;
}
///
/// 集合对象添加到list左边
///
///
///
///
///
public async Task<long> SetLists(string key, List values)
{
var result = 0L;
try
{
var jsonOption = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
var db = this.GetDb();
foreach (var item in values)
{
var _str = JsonConvert.SerializeObject(item, jsonOption);
result += await db.ListLeftPushAsync(key, _str);
}
return result;
}
catch (Exception ex) { }
return result;
}
///
/// 单个对象添加到list左边
///
///
///
///
///
public async Task<long> SetList(string key, T value)
{
var result = 0L;
try
{
result = await this.SetLists(key, new List { value });
}
catch (Exception ex) { }
return result;
}
#endregion
#region 额外扩展
public async Taskstring>> MatchKeys(params string[] paramArr)
{
var list = new List<string>();
try
{
var result = await this.ExecuteAsync("keys", paramArr);
var valArr = ((RedisValue[])result);
foreach (var item in valArr)
{
list.Add(item);
}
}
catch (Exception ex) { }
return list;
}
///
/// 执行redis原生命令
///
///
///
///
public async Task ExecuteAsync(string cmd, params string[] paramArr)
{
try
{
var db = this.GetDb();
return await db.ExecuteAsync(cmd, paramArr);
}
catch (Exception ex) { }
return default(RedisResult);
}
///
/// 手动回收管理器对象
///
public void Dispose()
{
this.Dispose(_redis);
}
public void Dispose(ConnectionMultiplexer con)
{
if (con != null)
{
con.Close();
con.Dispose();
}
}
#endregion
#endregion
}