开篇叙
本篇将和大家分享一下秒杀商品活动架构,采用的架构方案正如标题名称.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 try 10 { 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 try 32 { 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 try 113 { 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 /// 存储对象数据到string 142 /// </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 try 151 { 152 var jsonOption = new JsonSerializerSettings() 153 { 154 ReferenceLoopHandling = ReferenceLoopHandling.Ignore 155 }; 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 /// 是否存在key 167 /// </summary> 168 /// <typeparam name="T"></typeparam> 169 /// <param name="key"></param> 170 /// <returns></returns> 171 public async Task<bool> KeyExists(string key) 172 { 173 try 174 { 175 var db = this.GetDb(); 176 return await db.KeyExistsAsync(key); 177 } 178 catch (Exception ex) { } 179 return false; 180 } 181 182 #endregion 183 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 try 195 { 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 try 214 { 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 /// 设置hash 229 /// </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 try 239 { 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 try 257 { 258 var jsonOption = new JsonSerializerSettings() 259 { 260 ReferenceLoopHandling = ReferenceLoopHandling.Ignore 261 }; 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 try 285 { 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 try 305 { 306 var jsonOption = new JsonSerializerSettings() 307 { 308 ReferenceLoopHandling = ReferenceLoopHandling.Ignore 309 }; 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 /// 设置hash 323 /// </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 try 333 { 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 try 351 { 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 try 376 { 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 try 400 { 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 #endregion 416 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 try 428 { 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 try 446 { 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 try 469 { 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 try 490 { 491 var jsonOption = new JsonSerializerSettings() 492 { 493 ReferenceLoopHandling = ReferenceLoopHandling.Ignore 494 }; 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 try 518 { 519 result = await this.SetLists(key, new List<T> { value }); 520 } 521 catch (Exception ex) { } 522 return result; 523 } 524 525 526 #endregion 527 528 #region 额外扩展 529 530 public async Task<List<string>> MatchKeys(params string[] paramArr) 531 { 532 var list = new List<string>(); 533 try 534 { 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 try 556 { 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 #endregion 582 583 #endregion 584 }