很多應用上都有用戶簽到的功能,尤其是配合積分系統一起使用。現在有以下需求:
- 簽到1天得1積分,連續簽到2天得2積分,3天得3積分,3天以上均得3積分等。
- 如果連續簽到中斷,則重置計數,每月重置計數。
- 顯示用戶某月的簽到次數和首次簽到時間。
- 在日歷控件上展示用戶每月簽到,可以切換年月顯示。
- ...
功能分析
對于用戶簽到數據,如果直接采用數據庫存儲,當出現高并發訪問時,對數據庫壓力會很大,例如雙十一簽到活動。這時候應該采用緩存,以減輕數據庫的壓力,Redis是高性能的內存數據庫,適用于這樣的場景。
如果采用String類型保存,當用戶數量大時,內存開銷就非常大。
如果采用集合類型保存,例如Set、Hash,查詢用戶某個范圍的數據時,查詢效率又不高。
Redis提供的數據類型BitMap(位圖),每個bit位對應0和1兩個狀態。雖然內部還是采用String類型存儲,但Redis提供了一些指令用于直接操作BitMap,可以把它看作一個bit數組,數組的下標就是偏移量。
它的優點是內存開銷小,效率高且操作簡單,很適合用于簽到這類場景。缺點在于位計算和位表示數值的局限。如果要用位來做業務數據記錄,就不要在意value的值。
Redis提供了以下幾個指令用于操作BitMap:
命令 | 說明 | 可用版本 | 時間復雜度 |
---|---|---|---|
SETBIT | 對 key 所儲存的字符串值,設置或清除指定偏移量上的位(bit)。 | >= 2.2.0 | O(1) |
GETBIT | 對 key 所儲存的字符串值,獲取指定偏移量上的位(bit)。 | >= 2.2.0 | O(1) |
BITCOUNT | 計算給定字符串中,被設置為 1 的比特位的數量。 | >= 2.6.0 | O(N) |
BITPOS | 返回位圖中第一個值為 bit 的二進制位的位置。 | >= 2.8.7 | O(N) |
BITOP | 對一個或多個保存二進制位的字符串 key 進行位元操作。 | >= 2.6.0 | O(N) |
BITFIELD | BITFIELD 命令可以在一次調用中同時對多個位范圍進行操作。 | >= 3.2.0 | O(1) |
考慮到每月要重置連續簽到次數,最簡單的方式是按用戶每月存一條簽到數據。Key的格式為 u:sign:{uid}:{yyyMM},而Value則采用長度為4個字節的(32位)的BitMap(最大月份只有31天)。BitMap的每一位代表一天的簽到,1表示已簽,0表示未簽。
例如 u:sign:1225:202101 表示ID=1225的用戶在2021年1月的簽到記錄
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# 用戶1月6號簽到 SETBIT u:sign:1225:202101 5 1 # 偏移量是從0開始,所以要把6減1 # 檢查1月6號是否簽到 GETBIT u:sign:1225:202101 5 # 偏移量是從0開始,所以要把6減1 # 統計1月份的簽到次數 BITCOUNT u:sign:1225:202101 # 獲取1月份前31天的簽到數據 BITFIELD u:sign:1225:202101 get u31 0 # 獲取1月份首次簽到的日期 BITPOS u:sign:1225:202101 1 # 返回的首次簽到的偏移量,加上1即為當月的某一天 |
示例代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
|
using StackExchange.Redis; using System; using System.Collections.Generic; using System.Linq; /** * 基于Redis Bitmap的用戶簽到功能實現類 * * 實現功能: * 1. 用戶簽到 * 2. 檢查用戶是否簽到 * 3. 獲取當月簽到次數 * 4. 獲取當月連續簽到次數 * 5. 獲取當月首次簽到日期 * 6. 獲取當月簽到情況 */ public class UserSignDemo { private IDatabase _db; public UserSignDemo(IDatabase db) { _db = db; } /** * 用戶簽到 * * @param uid 用戶ID * @param date 日期 * @return 之前的簽到狀態 */ public bool DoSign( int uid, DateTime date) { int offset = date.Day - 1; return _db.StringSetBit(BuildSignKey(uid, date), offset, true ); } /** * 檢查用戶是否簽到 * * @param uid 用戶ID * @param date 日期 * @return 當前的簽到狀態 */ public bool CheckSign( int uid, DateTime date) { int offset = date.Day - 1; return _db.StringGetBit(BuildSignKey(uid, date), offset); } /** * 獲取用戶簽到次數 * * @param uid 用戶ID * @param date 日期 * @return 當前的簽到次數 */ public long GetSignCount( int uid, DateTime date) { return _db.StringBitCount(BuildSignKey(uid, date)); } /** * 獲取當月連續簽到次數 * * @param uid 用戶ID * @param date 日期 * @return 當月連續簽到次數 */ public long GetContinuousSignCount( int uid, DateTime date) { int signCount = 0; string type = $ "u{date.Day}" ; // 取1號到當天的簽到狀態 RedisResult result = _db.Execute( "BITFIELD" , (RedisKey)BuildSignKey(uid, date), "GET" , type, 0); if (!result.IsNull) { var list = ( long [])result; if (list.Length > 0) { // 取低位連續不為0的個數即為連續簽到次數,需考慮當天尚未簽到的情況 long v = list[0]; for ( int i = 0; i < date.Day; i++) { if (v >> 1 << 1 == v) { // 低位為0且非當天說明連續簽到中斷了 if (i > 0) break ; } else { signCount += 1; } v >>= 1; } } } return signCount; } /** * 獲取當月首次簽到日期 * * @param uid 用戶ID * @param date 日期 * @return 首次簽到日期 */ public DateTime? GetFirstSignDate( int uid, DateTime date) { long pos = _db.StringBitPosition(BuildSignKey(uid, date), true ); return pos < 0 ? null : date.AddDays(date.Day - ( int )(pos + 1)); } /** * 獲取當月簽到情況 * * @param uid 用戶ID * @param date 日期 * @return Key為簽到日期,Value為簽到狀態的Map */ public Dictionary< string , bool > GetSignInfo( int uid, DateTime date) { Dictionary< string , bool > signMap = new Dictionary< string , bool >(date.Day); string type = $ "u{GetDayOfMonth(date)}" ; RedisResult result = _db.Execute( "BITFIELD" , (RedisKey)BuildSignKey(uid, date), "GET" , type, 0); if (!result.IsNull) { var list = ( long [])result; if (list.Length > 0) { // 由低位到高位,為0表示未簽,為1表示已簽 long v = list[0]; for ( int i = GetDayOfMonth(date); i > 0; i--) { DateTime d = date.AddDays(i - date.Day); signMap.Add(FormatDate(d, "yyyy-MM-dd" ), v >> 1 << 1 != v); v >>= 1; } } } return signMap; } private static string FormatDate(DateTime date) { return FormatDate(date, "yyyyMM" ); } private static string FormatDate(DateTime date, string pattern) { return date.ToString(pattern); } /** * 構建簽到Key * * @param uid 用戶ID * @param date 日期 * @return 簽到Key */ private static string BuildSignKey( int uid, DateTime date) { return $ "u:sign:{uid}:{FormatDate(date)}" ; } /** * 獲取月份天數 * * @param date 日期 * @return 天數 */ private static int GetDayOfMonth(DateTime date) { if (date.Month == 2) { return 28; } if ( new int [] { 1, 3, 5, 7, 8, 10, 12 }.Contains(date.Month)) { return 31; } return 30; } static void Main( string [] args) { ConnectionMultiplexer connection = ConnectionMultiplexer.Connect( "192.168.0.104:7001,password=123456" ); UserSignDemo demo = new UserSignDemo(connection.GetDatabase()); DateTime today = DateTime.Now; int uid = 1225; { // doSign bool signed = demo.DoSign(uid, today); if (signed) { Console.WriteLine( "您已簽到:" + FormatDate(today, "yyyy-MM-dd" )); } else { Console.WriteLine( "簽到完成:" + FormatDate(today, "yyyy-MM-dd" )); } } { // checkSign bool signed = demo.CheckSign(uid, today); if (signed) { Console.WriteLine( "您已簽到:" + FormatDate(today, "yyyy-MM-dd" )); } else { Console.WriteLine( "尚未簽到:" + FormatDate(today, "yyyy-MM-dd" )); } } { // getSignCount long count = demo.GetSignCount(uid, today); Console.WriteLine( "本月簽到次數:" + count); } { // getContinuousSignCount long count = demo.GetContinuousSignCount(uid, today); Console.WriteLine( "連續簽到次數:" + count); } { // getFirstSignDate DateTime? date = demo.GetFirstSignDate(uid, today); if (date.HasValue) { Console.WriteLine( "本月首次簽到:" + FormatDate(date.Value, "yyyy-MM-dd" )); } else { Console.WriteLine( "本月首次簽到:無" ); } } { // getSignInfo Console.WriteLine( "當月簽到情況:" ); Dictionary< string , bool > signInfo = new Dictionary< string , bool >(demo.GetSignInfo(uid, today)); foreach (var entry in signInfo) { Console.WriteLine(entry.Key + ": " + (entry.Value ? "√" : "-" )); } } } } |
運行結果
更多應用場景
- 統計活躍用戶:把日期作為Key,把用戶ID作為offset,1表示當日活躍,0表示當日不活躍。還能使用位計算得到日活、月活、留存率等數據。
- 用戶在線狀態:跟統計活躍用戶一樣。
總結
- 位圖優點是內存開銷小,效率高且操作簡單;缺點是位計算和位表示數值的局限。
- 位圖適合二元狀態的場景,例如用戶簽到、在線狀態等場景。
- String類型最大長度為512M。 注意SETBIT時的偏移量,當偏移量很大時,可能會有較大耗時。 位圖不是絕對的好,有時可能更浪費空間。
- 如果位圖很大,建議分拆鍵。如果要使用BITOP,建議讀取到客戶端再進行位計算。
參考資料
Redis:Bitmap的setbit,getbit,bitcount,bitop等使用與應用場景
BITFIELD SET command is not working
到此這篇關于Redis基于Bitmap實現用戶簽到功能的文章就介紹到這了,更多相關Redis Bitmap用戶簽到內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.cnblogs.com/liang24/p/14449835.html