首页 > 代码库 > .NetCore+Jexus代理+Redis模拟秒杀商品活动

.NetCore+Jexus代理+Redis模拟秒杀商品活动

开篇叙

本篇将和大家分享一下秒杀商品活动架构,采用的架构方案正如标题名称.NetCore+Jexus代理+Redis,由于精力有限所以这里只设计到商品添加,抢购,订单查询,处理队列抢购订单的功能;有不足或者不够详细的还请见谅,顺手点个推荐也不错;

a. 秒杀流程

b. 封装StackExchange.Redis的使用类

c. Ubuntu16.04上使用Jexus搭建代理完成分布式部署

d. NetCore写实时监控队列服务

秒杀架构设计图︿( ̄︶ ̄)︿三幅

1. 一般业务性架构

技术分享

 

2. 后端分布式架构

技术分享

 

3. 整站分布式

技术分享

 

项目工程结构描述

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 

.NetCore写处理秒杀活动队列的服务

这个处理队列服务处理流程:模糊匹配Redis中每种商品的队列key-》开启不同商品的处理队列任务-》处理秒杀订单-》更新库存和秒杀订单状态;

a. 模糊匹配Redis中每种商品的队列key:这里采用的是StackExchange.Redis中指定redis原生命令的方法来获取匹配队列key,设计的代码如下:

 1 /// <summary> 2         /// 模糊匹配redis中的key 3         /// </summary> 4         /// <param name="paramArr"></param> 5         /// <returns></returns> 6         public async Task<List<string>> MatchKeys(params string[] paramArr) 7         { 8             var list = new List<string>(); 9             try10             {11                 var result = await this.ExecuteAsync("keys", paramArr);12 13                 var valArr = ((RedisValue[])result);14                 foreach (var item in valArr)15                 {16                     list.Add(item);17                 }18             }19             catch (Exception ex) { }20             return list;21         }22 23         /// <summary>24         /// 执行redis原生命令25         /// </summary>26         /// <param name="cmd"></param>27         /// <param name="paramArr"></param>28         /// <returns></returns>29         public async Task<RedisResult> ExecuteAsync(string cmd, params string[] paramArr)30         {31             try32             {33                 var db = this.GetDb();34                 return await db.ExecuteAsync(cmd, paramArr);35             }36             catch (Exception ex) { }37             return default(RedisResult);38         }

b. 开启不同商品的处理队列任务:通过Task.Factory.StartNew(action,object)方法开启不同商品的处理秒杀订单的任务;

c. 更新库存和秒杀订单状态:由于抢购商品要求库存剩余实时性,所以每处理一个抢购订单,需要对该商品减去相应的库存和修改秒杀订单的状态方便用户查看秒杀结果;

d. 处理队列具体的实现代码可以去git看下,个人觉得还是有用的:https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Server/Program.cs

使用Jexus代理部署分布式站点和接口

这里部署的代理采用的是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或许直接用我这个封装类是个不错的打算;

  1 public class StackRedis : IDisposable  2     {  3         #region 配置属性   基于 StackExchange.Redis 封装  4         //连接串 (注:IP:端口,属性=,属性=)  5         public string _ConnectionString = "127.0.0.1:6377,password=shenniubuxing3";  6         //操作的库(注:默认0库)  7         public int _Db = 0;  8         #endregion  9  10         #region 管理器对象 11  12         /// <summary> 13         /// 获取redis操作类对象 14         /// </summary> 15         private static StackRedis _StackRedis; 16         private static object _locker_StackRedis = new object(); 17         public static StackRedis Current 18         { 19             get 20             { 21                 if (_StackRedis == null) 22                 { 23                     lock (_locker_StackRedis) 24                     { 25                         _StackRedis = _StackRedis ?? new StackRedis(); 26                         return _StackRedis; 27                     } 28                 } 29  30                 return _StackRedis; 31             } 32         } 33  34         /// <summary> 35         /// 获取并发链接管理器对象 36         /// </summary> 37         private static ConnectionMultiplexer _redis; 38         private static object _locker = new object(); 39         public ConnectionMultiplexer Manager 40         { 41             get 42             { 43                 if (_redis == null) 44                 { 45                     lock (_locker) 46                     { 47                         _redis = _redis ?? GetManager(this._ConnectionString); 48                         return _redis; 49                     } 50                 } 51  52                 return _redis; 53             } 54         } 55  56         /// <summary> 57         /// 获取链接管理器 58         /// </summary> 59         /// <param name="connectionString"></param> 60         /// <returns></returns> 61         public ConnectionMultiplexer GetManager(string connectionString) 62         { 63             return ConnectionMultiplexer.Connect(connectionString); 64         } 65  66         /// <summary> 67         /// 获取操作数据库对象 68         /// </summary> 69         /// <returns></returns> 70         public IDatabase GetDb() 71         { 72             return Manager.GetDatabase(_Db); 73         } 74         #endregion 75  76         #region 操作方法 77  78         #region string 操作 79  80         /// <summary> 81         /// 根据Key移除 82         /// </summary> 83         /// <param name="key"></param> 84         /// <returns></returns> 85         public async Task<bool> Remove(string key) 86         { 87             var db = this.GetDb(); 88  89             return await db.KeyDeleteAsync(key); 90         } 91  92         /// <summary> 93         /// 根据key获取string结果 94         /// </summary> 95         /// <param name="key"></param> 96         /// <returns></returns> 97         public async Task<string> Get(string key) 98         { 99             var db = this.GetDb();100             return await db.StringGetAsync(key);101         }102 103         /// <summary>104         /// 根据key获取string中的对象105         /// </summary>106         /// <typeparam name="T"></typeparam>107         /// <param name="key"></param>108         /// <returns></returns>109         public async Task<T> Get<T>(string key)110         {111             var t = default(T);112             try113             {114                 var _str = await this.Get(key);115                 if (string.IsNullOrWhiteSpace(_str)) { return t; }116 117                 t = JsonConvert.DeserializeObject<T>(_str);118             }119             catch (Exception ex) { }120             return t;121         }122 123         /// <summary>124         /// 存储string数据125         /// </summary>126         /// <param name="key"></param>127         /// <param name="value"></param>128         /// <param name="expireMinutes"></param>129         /// <returns></returns>130         public async Task<bool> Set(string key, string value, int expireMinutes = 0)131         {132             var db = this.GetDb();133             if (expireMinutes > 0)134             {135                 return db.StringSet(key, value, TimeSpan.FromMinutes(expireMinutes));136             }137             return await db.StringSetAsync(key, value);138         }139 140         /// <summary>141         /// 存储对象数据到string142         /// </summary>143         /// <typeparam name="T"></typeparam>144         /// <param name="key"></param>145         /// <param name="value"></param>146         /// <param name="expireMinutes"></param>147         /// <returns></returns>148         public async Task<bool> Set<T>(string key, T value, int expireMinutes = 0)149         {150             try151             {152                 var jsonOption = new JsonSerializerSettings()153                 {154                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore155                 };156                 var _str = JsonConvert.SerializeObject(value, jsonOption);157                 if (string.IsNullOrWhiteSpace(_str)) { return false; }158 159                 return await this.Set(key, _str, expireMinutes);160             }161             catch (Exception ex) { }162             return false;163         }164 165         /// <summary>166         /// 是否存在key167         /// </summary>168         /// <typeparam name="T"></typeparam>169         /// <param name="key"></param>170         /// <returns></returns>171         public async Task<bool> KeyExists(string key)172         {173             try174             {175                 var db = this.GetDb();176                 return await db.KeyExistsAsync(key);177             }178             catch (Exception ex) { }179             return false;180         }181 182         #endregion183 184         #region hash操作185 186         /// <summary>187         /// 是否存在hash的列188         /// </summary>189         /// <param name="key"></param>190         /// <param name="filedKey"></param>191         /// <returns></returns>192         public async Task<bool> HashFieldExists(string key, string filedKey)193         {194             try195             {196                 if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(filedKey)) { return false; }197 198                 var result = await this.HashFieldsExists(key, new Dictionary<string, bool> { { filedKey, false } });199                 return result[filedKey];200             }201             catch (Exception ex) { }202             return false;203         }204 205         /// <summary>206         /// 是否存在hash的列集合207         /// </summary>208         /// <param name="key"></param>209         /// <param name="dics"></param>210         /// <returns></returns>211         public async Task<Dictionary<string, bool>> HashFieldsExists(string key, Dictionary<string, bool> dics)212         {213             try214             {215                 if (dics.Count <= 0) { return dics; }216 217                 var db = this.GetDb();218                 foreach (var fieldKey in dics.Keys)219                 {220                     dics[fieldKey] = await db.HashExistsAsync(key, fieldKey);221                 }222             }223             catch (Exception ex) { }224             return dics;225         }226 227         /// <summary>228         /// 设置hash229         /// </summary>230         /// <typeparam name="T"></typeparam>231         /// <param name="key"></param>232         /// <param name="filedKey"></param>233         /// <param name="t"></param>234         /// <returns></returns>235         public async Task<long> SetOrUpdateHashsField<T>(string key, string filedKey, T t, bool isAdd = true)236         {237             var result = 0L;238             try239             {240                 return await this.SetOrUpdateHashsFields<T>(key, new Dictionary<string, T> { { filedKey, t } }, isAdd);241             }242             catch (Exception ex) { }243             return result;244         }245 246         /// <summary>247         /// 设置hash集合,添加和更新操作248         /// </summary>249         /// <typeparam name="T"></typeparam>250         /// <param name="key"></param>251         /// <param name="dics"></param>252         /// <returns></returns>253         public async Task<long> SetOrUpdateHashsFields<T>(string key, Dictionary<string, T> dics, bool isAdd = true)254         {255             var result = 0L;256             try257             {258                 var jsonOption = new JsonSerializerSettings()259                 {260                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore261                 };262                 var db = this.GetDb();263                 foreach (var fieldKey in dics.Keys)264                 {265                     var item = dics[fieldKey];266                     var _str = JsonConvert.SerializeObject(item, jsonOption);267                     result += await db.HashSetAsync(key, fieldKey, _str) ? 1 : 0;268                     if (!isAdd) { result++; }269                 }270                 return result;271             }272             catch (Exception ex) { }273             return result;274         }275 276         /// <summary>277         /// 移除hash的列278         /// </summary>279         /// <param name="key"></param>280         /// <param name="filedKey"></param>281         /// <returns></returns>282         public async Task<bool> RemoveHashField(string key, string filedKey)283         {284             try285             {286                 if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(filedKey)) { return false; }287 288                 var result = await this.RemoveHashFields(key, new Dictionary<string, bool> { { filedKey, false } });289                 return result[filedKey];290             }291             catch (Exception ex) { }292             return false;293         }294 295         /// <summary>296         /// 异常hash的列集合297         /// </summary>298         /// <param name="key"></param>299         /// <param name="dics"></param>300         /// <returns></returns>301         public async Task<Dictionary<string, bool>> RemoveHashFields(string key, Dictionary<string, bool> dics)302         {303 304             try305             {306                 var jsonOption = new JsonSerializerSettings()307                 {308                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore309                 };310                 var db = this.GetDb();311                 foreach (var fieldKey in dics.Keys)312                 {313                     dics[fieldKey] = await db.HashDeleteAsync(key, fieldKey);314                 }315                 return dics;316             }317             catch (Exception ex) { }318             return dics;319         }320 321         /// <summary>322         /// 设置hash323         /// </summary>324         /// <typeparam name="T"></typeparam>325         /// <param name="key"></param>326         /// <param name="filedKey"></param>327         /// <param name="t"></param>328         /// <returns></returns>329         public async Task<T> GetHashField<T>(string key, string filedKey)330         {331             var t = default(T);332             try333             {334                 var dics = await this.GetHashFields<T>(key, new Dictionary<string, T> { { filedKey, t } });335                 return dics[filedKey];336             }337             catch (Exception ex) { }338             return t;339         }340 341         /// <summary>342         /// 获取hash的列值集合343         /// </summary>344         /// <typeparam name="T"></typeparam>345         /// <param name="key"></param>346         /// <param name="dics"></param>347         /// <returns></returns>348         public async Task<Dictionary<string, T>> GetHashFields<T>(string key, Dictionary<string, T> dics)349         {350             try351             {352                 var db = this.GetDb();353                 foreach (var fieldKey in dics.Keys)354                 {355                     var str = await db.HashGetAsync(key, fieldKey);356                     if (string.IsNullOrWhiteSpace(str)) { continue; }357 358                     dics[fieldKey] = JsonConvert.DeserializeObject<T>(str);359                 }360                 return dics;361             }362             catch (Exception ex) { }363             return dics;364         }365 366         /// <summary>367         /// 获取hash的key的所有列的值368         /// </summary>369         /// <typeparam name="T"></typeparam>370         /// <param name="key"></param>371         /// <returns></returns>372         public async Task<Dictionary<string, T>> GetHashs<T>(string key)373         {374             var dic = new Dictionary<string, T>();375             try376             {377                 var db = this.GetDb();378 379                 var hashFiles = await db.HashGetAllAsync(key);380                 foreach (var field in hashFiles)381                 {382                     dic[field.Name] = JsonConvert.DeserializeObject<T>(field.Value);383                 }384                 return dic;385             }386             catch (Exception ex) { }387             return dic;388         }389 390         /// <summary>391         /// 获取hash的Key的所有列的值的list集合392         /// </summary>393         /// <typeparam name="T"></typeparam>394         /// <param name="key"></param>395         /// <returns></returns>396         public async Task<List<T>> GetHashsToList<T>(string key)397         {398             var list = new List<T>();399             try400             {401                 var db = this.GetDb();402 403                 var hashFiles = await db.HashGetAllAsync(key);404                 foreach (var field in hashFiles)405                 {406                     var item = JsonConvert.DeserializeObject<T>(field.Value);407                     if (item == null) { continue; }408                     list.Add(item);409                 }410             }411             catch (Exception ex) { }412             return list;413         }414 415         #endregion416 417         #region List操作(注:可以当做队列使用)418 419         /// <summary>420         /// list长度421         /// </summary>422         /// <typeparam name="T"></typeparam>423         /// <param name="key"></param>424         /// <returns></returns>425         public async Task<long> GetListLen<T>(string key)426         {427             try428             {429                 var db = this.GetDb();430                 return await db.ListLengthAsync(key);431             }432             catch (Exception ex) { }433             return 0;434         }435 436         /// <summary>437         /// 获取List数据438         /// </summary>439         /// <typeparam name="T"></typeparam>440         /// <param name="key"></param>441         /// <returns></returns>442         public async Task<List<T>> GetList<T>(string key)443         {444             var t = new List<T>();445             try446             {447                 var db = this.GetDb();448                 var _values = await db.ListRangeAsync(key);449                 foreach (var item in _values)450                 {451                     if (string.IsNullOrWhiteSpace(item)) { continue; }452                     t.Add(JsonConvert.DeserializeObject<T>(item));453                 }454             }455             catch (Exception ex) { }456             return t;457         }458 459         /// <summary>460         /// 获取队列出口数据并移除461         /// </summary>462         /// <typeparam name="T"></typeparam>463         /// <param name="key"></param>464         /// <returns></returns>465         public async Task<T> GetListAndPop<T>(string key)466         {467             var t = default(T);468             try469             {470                 var db = this.GetDb();471                 var _str = await db.ListRightPopAsync(key);472                 if (string.IsNullOrWhiteSpace(_str)) { return t; }473                 t = JsonConvert.DeserializeObject<T>(_str);474             }475             catch (Exception ex) { }476             return t;477         }478 479         /// <summary>480         /// 集合对象添加到list左边481         /// </summary>482         /// <typeparam name="T"></typeparam>483         /// <param name="key"></param>484         /// <param name="values"></param>485         /// <returns></returns>486         public async Task<long> SetLists<T>(string key, List<T> values)487         {488             var result = 0L;489             try490             {491                 var jsonOption = new JsonSerializerSettings()492                 {493                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore494                 };495                 var db = this.GetDb();496                 foreach (var item in values)497                 {498                     var _str = JsonConvert.SerializeObject(item, jsonOption);499                     result += await db.ListLeftPushAsync(key, _str);500                 }501                 return result;502             }503             catch (Exception ex) { }504             return result;505         }506 507         /// <summary>508         /// 单个对象添加到list左边509         /// </summary>510         /// <typeparam name="T"></typeparam>511         /// <param name="key"></param>512         /// <param name="value"></param>513         /// <returns></returns>514         public async Task<long> SetList<T>(string key, T value)515         {516             var result = 0L;517             try518             {519                 result = await this.SetLists(key, new List<T> { value });520             }521             catch (Exception ex) { }522             return result;523         }524 525 526         #endregion527 528         #region 额外扩展529 530         public async Task<List<string>> MatchKeys(params string[] paramArr)531         {532             var list = new List<string>();533             try534             {535                 var result = await this.ExecuteAsync("keys", paramArr);536 537                 var valArr = ((RedisValue[])result);538                 foreach (var item in valArr)539                 {540                     list.Add(item);541                 }542             }543             catch (Exception ex) { }544             return list;545         }546 547         /// <summary>548         /// 执行redis原生命令549         /// </summary>550         /// <param name="cmd"></param>551         /// <param name="paramArr"></param>552         /// <returns></returns>553         public async Task<RedisResult> ExecuteAsync(string cmd, params string[] paramArr)554         {555             try556             {557                 var db = this.GetDb();558                 return await db.ExecuteAsync(cmd, paramArr);559             }560             catch (Exception ex) { }561             return default(RedisResult);562         }563 564         /// <summary>565         /// 手动回收管理器对象566         /// </summary>567         public void Dispose()568         {569             this.Dispose(_redis);570         }571 572         public void Dispose(ConnectionMultiplexer con)573         {574             if (con != null)575             {576                 con.Close();577                 con.Dispose();578             }579         }580 581         #endregion582 583         #endregion584     }

.NetCore+Jexus代理+Redis模拟秒杀商品活动