[C#]使用RabbitMQ模拟抽奖系统的例子
背景:在实际的项目中,经常有客户需要做抽奖的活动,大部分的都是注册送产品、送红包这些需求。这都是有直接的利益效果,所以经常会遇见系统被盗刷的情况,每一次遇见这种项目的上线都是绷紧神经,客户又都喜欢在过节的时候上这种活动,有好多次放假前夕都是在解决这种事情,甚至有一次的活动短信接口直接被恶意刷爆了。在这种恶意请求下对系统并发性要求就很高,但是即使做多方面的完善,有一个问题始终得不到根本的解决,那就是奖品池数量的控制,总是会出现超兑,或者一个奖品被多个人兑走的问题。之后尝试了多种及方法,例如:限制IP,限制次数等等。后来最有效的解决方法就是使用Redis锁住奖品逻辑,但是这种实现有点复杂,也不是很友好,因此就想到了使用消息队列的优势来实现此功能。
做这个示例首先是为了学习,再者也是留下学习的笔记,不然后面又遗忘掉了
这个示例是一边学习RabbitMQ,一边实现自己的需求功能的。主要功能有【投放奖品】、【模拟多户请求】、【模拟用户抽奖】,并且在这些操作中及时的展示各个队列中数据的数量变化,先上一张效果图:
示例测试下来,始终能保证奖品的数量与实际的中奖人数是一致的,不会多出一个中奖人,也不会出现有多个人中同一个奖品的问题。
实现方式主要就是多线程模拟用户请求,结合RabbitMQ,其中还是用了RabbitMQ的在线API进行数据的监控展示。
实现思路:
1:先将奖品丢入奖品池;
1 #region 投放奖品 2 /// <summary> 3 /// 投放奖品 4 /// </summary> 5 /// <param name="sender"></param> 6 /// <param name="e"></param> 7 private void btn1_Click(object sender, EventArgs e) 8 { 9 try 10 { 11 SetSendfigModel(PrizeQueueName); //设置队列信息(奖品池) 12 new Thread(SetPrize) { IsBackground = true }.Start(); 13 } 14 catch (Exception ex) 15 { 16 MessageBox.Show(ex.Message, "出错了", MessageBoxButtons.OK); 17 } 18 } 19 20 /// <summary> 21 /// 22 /// </summary> 23 private void SetPrize() 24 { 25 string value = string.Empty; 26 for (int i = 1; i <= PrizeCount; i++) 27 { 28 PrizeInfo prize = new PrizeInfo 29 { 30 Id = i, 31 Name = "我是奖品" + i, 32 Type = 1, 33 PrizeNo = DateTime.Now.ToString("hhmmssfff"), 34 Total = PrizeCount, 35 Balance = PrizeCount 36 }; 37 value = JsonConvert.SerializeObject(prize); 38 RabbitSend.Send(prize); 39 ShowSysMessage($"我骄傲,我是奖品:{i}/{PrizeCount}"); 40 } 41 ShowSysMessage("奖品投放完成"); 42 } 43 #endregion
2:模拟多用户页面请求
利用多线程实现用户随机访问抽奖系统,这里将所有用户的信息来了就做插入到用户池当中,后续进行抽奖的时候再从用户池中顺序取出。
1 #region 模拟多用户页面请求 2 /// <summary> 3 /// 模拟多用户页面请求 4 /// </summary> 5 /// <param name="sender"></param> 6 /// <param name="e"></param> 7 private void btn2_Click(object sender, EventArgs e) 8 { 9 try 10 { 11 SetSendfigModel(UserQueueName); //设置队列信息(用户池) 12 ShowSysMessage("开始模拟多用户页面请求..."); 13 new Thread(ThreadFunction) { IsBackground = true }.Start(); 14 } 15 catch (Exception ex) 16 { 17 MessageBox.Show(ex.Message, "出错了", MessageBoxButtons.OK); 18 } 19 } 20 21 private const int threadLength = 8; 22 private static CancellationTokenSource cts = new CancellationTokenSource(); 23 24 /// <summary> 25 /// 26 /// </summary> 27 private void ThreadFunction() 28 { 29 cts = new CancellationTokenSource(); 30 TaskFactory taskFactory = new TaskFactory(); 31 Task[] tasks = new Task[threadLength]; 32 33 for (int i = 0; i < threadLength; i++) 34 { 35 Task t1 = Task.Factory.StartNew(delegate { ParallelFunction(cts.Token); }); 36 tasks.SetValue(t1, i); 37 } 38 taskFactory.ContinueWhenAll(tasks, TasksEnded, CancellationToken.None); 39 } 40 41 /// <summary> 42 /// 43 /// </summary> 44 /// <param name="tasks"></param> 45 void TasksEnded(Task[] tasks) 46 { 47 ShowSysMessage("所有任务已完成/或已取消!"); 48 } 49 50 /// <summary> 51 /// 52 /// </summary> 53 private void ParallelFunction(CancellationToken ct) 54 { 55 Parallel.For(0, 1000, item => 56 { 57 if (!ct.IsCancellationRequested) 58 { 59 string value = string.Empty; 60 UsersInfo user = new UsersInfo 61 { 62 Id = item, 63 Name = "我是:" + item 64 }; 65 value = Newtonsoft.Json.JsonConvert.SerializeObject(user); 66 ShowSysMessage($"进来了一位用户:{value}"); 67 RabbitSend.Send(user); 68 } 69 }); 70 } 71 #endregion
3:模拟多用户抽奖
从用户池中顺序取出一个用户进行奖品的锁定,锁定之后生成用户与奖品的关系,插入中奖池中。
1 #region 模拟多用户抽奖 2 3 /// <summary> 4 /// 模拟多用户抽奖 5 /// </summary> 6 /// <param name="sender"></param> 7 /// <param name="e"></param> 8 private void btn3_Click(object sender, EventArgs e) 9 { 10 //1:先去用户池中取出一个人 2 拿用户去抽一个奖品 3:将中奖人塞入中奖队列 11 new Thread(() => 12 { 13 for (int i = 0; i < 10000; i++) 14 { 15 SetReceivefigModel(UserQueueName);//设置队列信息(用户池) 16 RabbitReceive.BasicGet(LockUser); 17 } 18 19 //Parallel.For(0, 200000, item => 20 //{ 21 // RabbitReceive.BasicGet(LockUser); 22 //}); 23 }) 24 { IsBackground = true }.Start(); 25 } 26 27 /// <summary> 28 /// 先去用户池中取出一个人 29 /// </summary> 30 /// <param name="tp"></param> 31 private void LockUser(ValueTuple<bool, string, Dictionary<string, object>> tp) 32 { 33 try 34 { 35 if (tp.Item1) 36 { 37 ShowSysMessage($"锁定到一个用户:{tp.Item2}"); 38 UsersInfo user = JsonConvert.DeserializeObject<UsersInfo>(tp.Item2); 39 if (null != user) 40 { 41 Thread.Sleep(50); 42 LockPrize(user);//拿用户去抽一个奖品 43 } 44 } 45 else 46 { 47 ShowSysMessage(tp.Item2); 48 } 49 } 50 catch (Exception ex) 51 { 52 MessageBox.Show(ex.Message, "出错了", MessageBoxButtons.OK); 53 } 54 } 55 56 /// <summary> 57 /// 拿用户去抽一个奖品 58 /// </summary> 59 /// <param name="user"></param> 60 private void LockPrize(UsersInfo user) 61 { 62 SetReceivefigModel(PrizeQueueName);//设置队列信息(奖品池) 63 Dictionary<string, object> data = new Dictionary<string, object> { { "User", user } }; 64 RabbitReceive.BasicGet(LockPrize, data); 65 } 66 67 /// <summary> 68 /// 锁定奖品 69 /// </summary> 70 /// <param name="value"></param> 71 private void LockPrize(ValueTuple<bool, string, Dictionary<string, object>> tp) 72 { 73 try 74 { 75 if (tp.Item1) 76 { 77 UsersInfo user = tp.Item3["User"] as UsersInfo; 78 PrizeInfo prize = JsonConvert.DeserializeObject<PrizeInfo>(tp.Item2); 79 if (null != user && null != prize) 80 { 81 user.PrizeInfo = prize; 82 ShowSysMessage($"用户{user.Name}锁定到一个奖品:{tp.Item2}"); 83 PrizeUser(user);// 将中奖人塞入中奖队列 84 } 85 } 86 else 87 { 88 ShowSysMessage(tp.Item2); 89 } 90 } 91 catch (Exception ex) 92 { 93 MessageBox.Show(ex.Message, "出错了", MessageBoxButtons.OK); 94 } 95 } 96 97 /// <summary> 98 /// 将中奖人塞入中奖队列 99 /// </summary> 100 /// <param name="user"></param> 101 private void PrizeUser(UsersInfo user) 102 { 103 SetSendfigModel(PrizeUserQueueName); //设置队列信息(中奖人) 104 RabbitSend.Send(user); 105 Thread.Sleep(50); 106 } 107 #endregion
4:使用RabbitMQ的在线API进行数据的监控展示
1 #region 处理队列中数据 2 3 /// <summary> 4 /// 5 /// </summary> 6 private void LoadData() 7 { 8 System.Timers.Timer t = new System.Timers.Timer(3000); //实例化Timer类,设置间隔时间为10000毫秒; 9 t.Elapsed += new System.Timers.ElapsedEventHandler(InitRabbit); //到达时间的时候执行事件; 10 t.AutoReset = true; //设置是执行一次(false)还是一直执行(true); 11 t.Enabled = true; //是否执行System.Timers.Timer.Elapsed事件; 12 } 13 14 /// <summary> 15 /// 初始化队列中已有的数据 16 /// </summary> 17 /// <param name="source"></param> 18 /// <param name="e"></param> 19 private void InitRabbit(object source, System.Timers.ElapsedEventArgs e) 20 { 21 if (this.IsHandleCreated) 22 { 23 Invoke(new Action(() => 24 { 25 ShowLbUserUserExchanges(RabbitSendConfig.ExchangesApi); 26 ShowLbQueues(RabbitSendConfig.QueuesApi); 27 ShowLbBindings(RabbitSendConfig.BingdingsApi); 28 ShowSysMessage($"[{DateTime.Now}]数据已更新...................."); 29 })); 30 } 31 } 32 33 /// <summary> 34 /// 35 /// </summary> 36 /// <param name="apiUrl"></param> 37 private async void ShowLbUserUserExchanges(string apiUrl) 38 { 39 userExchanges = await GetListModel<List<ExchangeEntity>>(apiUrl); 40 } 41 42 /// <summary> 43 /// 44 /// </summary> 45 /// <param name="apiUrl"></param> 46 private async void ShowLbQueues(string apiUrl) 47 { 48 queues = await GetListModel<List<QueueEntity>>(apiUrl); 49 if (queues != null && queues.Any()) 50 { 51 lbQueues.Items.Clear(); 52 lbPrize.Text = "0"; 53 lbUser.Text = "0"; 54 lbPrizeUser.Text = "0"; 55 foreach (var queueEntity in queues) 56 { 57 lbQueues.Items.Add(queueEntity.name); 58 if (queueEntity.name == PrizeQueueName) 59 { 60 lbPrize.Text = queueEntity.messages_ready.ToString(); //奖品剩余数量 61 } 62 if (queueEntity.name == UserQueueName) 63 { 64 lbUser.Text = queueEntity.messages_ready.ToString(); //用户数量 65 } 66 if (queueEntity.name == PrizeUserQueueName) 67 { 68 lbPrizeUser.Text = queueEntity.messages_ready.ToString(); //中奖人数 69 } 70 } 71 } 72 else 73 { 74 lbQueues.Items.Clear(); 75 lbPrize.Text = "0"; 76 lbUser.Text = "0"; 77 lbPrizeUser.Text = "0"; 78 } 79 } 80 81 /// <summary> 82 /// 83 /// </summary> 84 /// <param name="apiUrl"></param> 85 private async void ShowLbBindings(string apiUrl) 86 { 87 bindings = await GetListModel<List<BindingEntity>>(apiUrl); 88 if (bindings != null) 89 { 90 lbBindings.Items.Clear(); 91 foreach (var bindingEntity in bindings) 92 { 93 lbBindings.Items.Add(string.Format("交换机:{0}---队列:{1}---Key:{2}", string.IsNullOrWhiteSpace(bindingEntity.source) ? "默认" : bindingEntity.source, bindingEntity.destination, bindingEntity.routing_key)); 94 } 95 } 96 else 97 { 98 lbBindings.Items.Clear(); 99 } 100 } 101 102 /// <summary> 103 /// 104 /// </summary> 105 /// <typeparam name="T"></typeparam> 106 /// <param name="apiUrl"></param> 107 /// <returns></returns> 108 private async Task<T> GetListModel<T>(string apiUrl) 109 { 110 string jsonContent = await ShowApiResult(apiUrl); 111 return JsonConvert.DeserializeObject<T>(jsonContent); 112 } 113 114 /// <summary> 115 /// 116 /// </summary> 117 /// <param name="apiUrl"></param> 118 /// <returns></returns> 119 private async Task<string> ShowApiResult(string apiUrl) 120 { 121 var response = await ShowHttpClientResult(apiUrl); 122 response.EnsureSuccessStatusCode(); 123 string responseBody = await response.Content.ReadAsStringAsync(); 124 return responseBody; 125 } 126 127 /// <summary> 128 /// 129 /// </summary> 130 /// <param name="Url"></param> 131 /// <returns></returns> 132 private async Task<HttpResponseMessage> ShowHttpClientResult(string Url) 133 { 134 var client = new HttpClient(); 135 var byteArray = Encoding.ASCII.GetBytes(string.Format("{0}:{1}", RabbitReceiveConfig.UserName, RabbitReceiveConfig.Password)); 136 client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); 137 HttpResponseMessage response = await client.GetAsync(Url); 138 return response; 139 } 140 #endregion
基本上大致的实现逻辑就是以上这些了,但是其实还有一个逻辑的问题我没有处理
这里要中奖用户是唯一的,实现这一点可以从两点入手
1:用户池用户信息唯一;
2:锁定奖品时要唯一;
这两点都可以实现这个逻辑,但是暂时还不知道RabbitMQ是否支持消息的唯一性,或者可以通过DB/Redis来实现。
其他具体的代码就不做展示,直接在附件中体现。
代码环境
win10 + Visual Studio Community 2017