本篇將和大家分享一下秒殺商品活動架構(gòu),采用的架構(gòu)方案正如標(biāo)題名稱.NetCore+Jexus代理+Redis,由于精力有限所以這里只設(shè)計到商品添加,搶購,訂單查詢,處理隊列搶購訂單的功能;有不足或者不夠詳細(xì)的還請見諒,順手點(diǎn)個推薦也不錯;
a. 秒殺流程
b. 封裝StackExchange.Redis的使用類
c. Ubuntu16.04上使用Jexus搭建代理完成分布式部署
d. NetCore寫實(shí)時監(jiān)控隊列服務(wù)
秒殺架構(gòu)設(shè)計圖︿( ̄︶ ̄)︿三幅
1. 一般業(yè)務(wù)性架構(gòu)
2. 后端分布式架構(gòu)
3. 整站分布式
項目工程結(jié)構(gòu)描述
a. 該項目git開源地址: https://github.com/shenniubuxing3/SeckillPro ,線上效果地址: http://www.lovexins.com:3333/
b. SeckillPro.Web:面向用戶的web站點(diǎn),主要提供商品展示,秒殺搶購,搶購結(jié)果,訂單列表等功能;
c. SeckillPro.Api:主要處理秒殺活動的請求,然后加入到秒殺隊列中,以及訂單狀態(tài)的查詢接口;
d. SeckillPro.Server:處理秒殺隊列的服務(wù);根據(jù)Redis模糊匹配key的方式,開啟多個商品秒殺的任務(wù),并處理秒殺請求和改變訂單搶購狀態(tài);
e. SeckillPro.Com:集成公共的方法;這里面前有操作Redis的list,hash,string的封裝類;
SeckillPro.Web商品后臺管理
對于商品活動來說,商品維護(hù)是必不可少的,由于這里商品維護(hù)的信息比較少,并且這里只加入到了RedisDb中,所以就不直接上代碼了;一個列表,一個添加僅此而已;這里就不再貼代碼了,如果你感興趣可以去我的git上面看源碼: https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Web/Controllers/HomeController.cs
SeckillPro.Web用戶端商品列表+秒殺請求+用戶訂單列表
商品列表和訂單列表沒有可以太多說的,一般訂單系統(tǒng)都有這兩個列表;關(guān)鍵點(diǎn)在于訂單秒殺流程中,咋們來簡單分析下面向客戶秒殺的流程需要注意的事項:
a. 限制秒殺開始時間和結(jié)束時間(測試未限制)
b. 未開始活動限制提交按鈕不可點(diǎn)(測試未限制)
c. 獲取真實(shí)剩余庫存限制秒殺提交(獲取redis中商品hash存儲的真實(shí)剩余量)
d. 把客戶的秒殺請求轉(zhuǎn)移到另外的api集群,以此提高面向客戶端的web站點(diǎn)并發(fā)承載率(測試項目中我直接指定4545端口的api測試)
這里就不再貼代碼了,如果你感興趣可以去我的git上面看看這部分源碼: https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Web/Controllers/HomeController.cs
.NetCore寫處理秒殺活動隊列的服務(wù)
這個處理隊列服務(wù)處理流程:模糊匹配Redis中每種商品的隊列key-》開啟不同商品的處理隊列任務(wù)-》處理秒殺訂單-》更新庫存和秒殺訂單狀態(tài);
a. 模糊匹配Redis中每種商品的隊列key:這里采用的是StackExchange.Redis中指定redis原生命令的方法來獲取匹配隊列key,設(shè)計的代碼如下:
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 /// 執(zhí)行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. 開啟不同商品的處理隊列任務(wù):通過Task.Factory.StartNew(action,object)方法開啟不同商品的處理秒殺訂單的任務(wù);
c. 更新庫存和秒殺訂單狀態(tài):由于搶購商品要求庫存剩余實(shí)時性,所以每處理一個搶購訂單,需要對該商品減去相應(yīng)的庫存和修改秒殺訂單的狀態(tài)方便用戶查看秒殺結(jié)果;
d. 處理隊列具體的實(shí)現(xiàn)代碼可以去git看下,個人覺得還是有用的:https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Server/Program.cs
使用Jexus代理部署分布式站點(diǎn)和接口
這里部署的代理采用的是Jexus代理;作為在linux和unix上部署.net程序?qū)嵱玫墓ぞ?,真的很感謝jexus作者;首先本篇講解的部署環(huán)境是ubunt16.04x64(至于這么安裝jexus可以參考上一篇分享文章),為了更直觀的看出來效果我在服務(wù)器上拷貝了兩份SeckillPro.Web發(fā)布的站點(diǎn),他們代碼都是一樣的只是分別把_Layout.cshtml試圖模板中加入了端口7777和8888,我就用這兩個端口來測試jexus的代理效果;
測試方便直接分別在兩個復(fù)制站點(diǎn)中執(zhí)行如下終端命令:dotnet SeckillPro.Web.dll http://ip:端口 ;一個監(jiān)聽7777端口一個監(jiān)聽8888;執(zhí)行命令效果圖:
監(jiān)聽7777和8888端口成功后,我們就可以直接在瀏覽器輸入:http://172.16.9.66:7777 訪問,正常情況下能夠看到如下圖示例:
單個站點(diǎn)訪問沒問題了,下面開始配置jexus代理;只需要在jexus/siteconf的配置文件中(我這里是default配置文件),增加如下設(shè)置:
注意reproxy參數(shù):
a. 第一個/表示根目錄,一般不變
b. 多個被代理地址使用‘,’隔開;
c. 被代理地址后面也同樣需要加/
此時我們配置完后,只需要啟動jexus就行了:./jws start (怎么啟動可以參考上一篇文章);當(dāng)啟動jws成功后,我們就能通過配置的80端口,來訪問SeckillPro.Web站點(diǎn)了,效果圖:
至于代理分發(fā)的策略暫不在本章的討論范圍內(nèi),如果可以建議去jexus官網(wǎng)了解下;同樣對于Seckill.Api我們也可以這樣部署,這里部署了個秒殺線上地址,有興趣的朋友可以點(diǎn)擊試試:http://www.lovexins.com:3333/ (注:這里沒有使用代理)
封裝StackExchange.Redis的使用類StackRedis.cs
其實(shí)這個在之前已經(jīng)分享過了,只不過只有操作string和list的分裝;本篇測試涉及到訂單查詢和商品查詢等功能,所以這里我又?jǐn)U展了對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 //操作的庫(注:默認(rèn)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 /// 獲取并發(fā)鏈接管理器對象 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 /// 獲取操作數(shù)據(jù)庫對象 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 /// 根據(jù)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 /// 根據(jù)key獲取string結(jié)果 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 /// 根據(jù)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數(shù)據(jù)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 /// 存儲對象數(shù)據(jù)到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 http://www.cnblogs.com/wangrudong003/p/7111789.html