一:需求背景
產品簽到活動需求,對于日活量不高的程序,可以直接使用MySQL去處理,當然數據量不大的話,簽到一次,數據庫簽到表保存一條記錄,也是可以的。但是如果類似京東等商城,那種簽到活動,日活好幾億,并且并發也很高,這樣不斷瘋狂進行數據庫讀寫操作,MySQL壓力是很大的。
那怎么解決呢?
二:解決方案
可以利用MySQL,定義一個int類型的字段,4byte=32位,存儲一個月的數據,每次簽到,改變這個字段,程序代碼進行二進制邏輯預算,數據庫該值存儲十進制,優化成一行數據存一個人一個月的簽到記錄。一般商城促銷活動簽到,一個月一個周期已經可以了。
三:代碼實現
代碼實現前,先來復習一下邏輯運算和簽到邏輯:
1.數據庫保存簽到記錄,int類型,4 byte = 32位可以存儲一周或者一個月數據
周1:0000 0001 == 1
周2:0000 0010 == 2
周3:0000 0100 == 4
周4:0000 1000 == 8
周5:0001 0000 == 16
周6:0010 0000 == 32
周7:0100 0000 == 64
2.異或 ^ 、與 & 、或 | 邏輯運算:
異或 ^:兩者不一樣,eg 1^0,0^1 得出結果就是 1,其他1^1,0^0都是0
與 &: 只有兩者都是是 1 ,eg 1&1 得出結果就是 1,其他都是0
或 |: 兩者有一個是 1 ,eg 1^0,0^1,1^1 得出結果就是 1,其他都是0
所以根據上面分析,可以得出以下模擬每天簽到的案例(ps:<< 表示左移):
第一天簽到:
1 << 0 == 0000 0001 這時數據庫存的是 1
第二天簽到:
1 << 1 == 0000 0010,此時數據保存前一天簽到 0000 0001,所以需要兩個數據合成一個,可以用 異或 ^ 或者 或 |
0000 0010 | 0000 0001 = 0000 0011 或者 0000 0001 ^ 0000 0010 = 0000 0011
這時數據庫存的是 0000 0011 = 3
第三天簽到:
1 << 2 == 0000 0100, 數據庫存的是 0000 0011
0000 0100 | 0000 0011 = 0000 0111 或者 0000 0100 ^ 0000 0011 = 0000 0111
這時數據庫存的是 0000 0111 = 7
第四天沒有簽到
第五天簽到:
1 << 4 == 0001 0000, 數據庫存的是 0000 0111
0001 0000 | 0000 0111 = 0001 0111 或者 0001 0000 ^ 0000 0111 = 0001 0111
........
業務層代碼實現,為了方便,代碼模擬一周為一個周期進行簽到:
/**
* 簽到并領取獎勵
* @param uid
* @param day 第幾天
* @return 簽到成功返回獎勵
*/
public Integer rewardReceiveByDB(String uid, int day) {
// 查詢當前一周,該用戶是否簽到記錄
LocalDate now = LocalDate.now();
LocalDateTime startTime = LocalDateTime.of(now.with(DayOfWeek.MONDAY), LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(now.with(DayOfWeek.SUNDAY), LocalTime.MAX);
// 查詢是否有本周簽到記錄,一周只有一條記錄
QueryWrApper<SignLog> wrapper = new QueryWrapper<>();
wrapper.eq("uid", uid)
.ge("ctime", startTime)
.le("ctime", endTime);
SignLog signLog = signLogService.getOne(wrapper);
if (signLog == null){
signLog = new SignLog();
signLog.setUid(uid);
signLog.setFlag(0);
}
/*
1 << 2 == 0000 0100, 數據庫存的是 0000 0011
0000 0100 | 0000 0011 = 0000 0111 或者 0000 0100 ^ 0000 0011 = 0000 0111
*/
int signFlag = signLog.getFlag();
// 二進制下標 0開始
signFlag = signFlag | (1 << (day - 1));
signLog.setFlag(signFlag);
signLogService.saveOrUpdate(signLog);
// Integer.bitCount直接統計,簽到了幾天,發獎勵
long signInDays = Integer.bitCount(signFlag);
// todo 根據業務需求,發送獎勵
return rewardMap.get(signInDays);
}
/**
* 查看用戶在某一天是否簽到了
* @param uid
* @param day 第幾天
* @return
*/
@Override
public boolean getSignStatus(String uid, int day) {
/**
* 判斷某一天 day 該用戶是否簽到
* 可用 某一天 day 簽到的 二進制值 跟 當前數據的值 進行 與& 操作,也就是二進制位只有 day 位置是 1,其他都是0,
* 如果跟它進行 與& 操作 如果數據庫值當天簽到 就會 等于 1
*/
LocalDate now = LocalDate.now();
LocalDateTime startTime = LocalDateTime.of(now.with(DayOfWeek.MONDAY), LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(now.with(DayOfWeek.SUNDAY), LocalTime.MAX);
// 查詢是否有本周簽到記錄,一周只有一條記錄
QueryWrapper<SignLog> wrapper = new QueryWrapper<>();
wrapper.eq("uid", uid)
.ge("ctime", startTime)
.le("ctime", endTime);
SignLog signLog = signLogService.getOne(wrapper);
if (signLog == null){
return false;
}
Integer flag = signLog.getFlag();
// 當天簽到的二進制值
int signFlag = 1 << (day - 1);
return (flag & signFlag) == 1;
}
四:總結
個人覺得,具體實現根據公司的業務體量來決定。但是條件允許的話,確實可以用位存儲來實現。其實redis的一個數據類型bitmap也是根據位來實現。方便一點的,可以知己義工redis的bitmap來實現,這樣完全就不用經過數據庫,但是如果需要歸因的話,數據庫保存發送簽到獎勵的記錄即可。
本人bitmap實現簽到
:https://blog.csdn.NET/qi_ming88/article/details/102716469