背景:在實際的項目中,經常有客戶需要做抽獎的活動,大部分的都是注冊送產品、送紅包這些需求。這都是有直接的利益效果,所以經常會遇見系統被盜刷的情況,每一次遇見這種項目的上線都是綳緊神經,客戶又都喜歡在過節的時候上這種活動,有好多次放假前夕都是在解決這種事情,甚至有一次的活動短信接口直接被惡意刷爆了。在這種惡意請求下對系統並發性要求就很高,但是即使做多方面的完善,有一個問題始終得不到根本的解決,那就是獎品池數量的控制,總是會出現超兌,或者一個獎品被多個人兌走的問題。之后嘗試了多種及方法,例如:限制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