用戶簽到
假如我們使用一張表來存儲用戶簽到信息,其結構應該如下:
CREATE TABLE `tb_sign` (`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',`user_id` bigint unsigned NOT NULL COMMENT '用戶id',`year` year NOT NULL COMMENT '簽到的年',`month` tinyint NOT NULL COMMENT '簽到的月',`date` date NOT NULL COMMENT '簽到的日期',`is_backup` tinyint unsigned DEFAULT NULL COMMENT '是否補簽',PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT
假設有1000萬用戶,平均每人每年簽到次數為10次,那么這張表一年的數據量為1億條。還是保守估計,因此,用數據庫表來存儲太過浪費內存空間。
并且每一個用戶簽到一次需要使用(8+8+1+1+3+1)共22字節的內存,并且沒有包括隱藏字段,一個月最多需要600多字節。
因此這種方式既耗內存,數據庫壓力還大。
那有沒有比較好的方法呢?
我們按照月來統計用戶簽到信息,簽到記錄為1,未簽到記錄為0,這樣我們只需要最多31bit就可以表示一個用戶一個月的簽到情況,非常節省空間,這種做法的核心思想就是把每一個比特位對應當月的每一天,形成了映射關系,用0和1表示業務狀態。
這種思路就叫做位圖(BitMap)。
而在redis底層是利用String類型數據結構實現BitMap,因此最大上限是512M,轉換為bit則是2^32個bit位。
BitMap用法
BitMap的操作命令有:
SETBIT
:向指定位置(offset)存入一個0或者1
GETBIT:
獲取指定位置(offset)的bit值
BITCOUNT:
統計BitMap中值為1的bit位的數量
BITFIELD:
操作(查詢、修改、自增)BitMap中bit數組中的指定位置(offset)的值
BITFIELD_RO:
獲取BitMap中bit數組,并以十進制形式返回
BITOP:
將多個BItMap的結果做位運算(與、或、異或)
BITPOS:
查找bit數組中指定范圍內的第一個0或1出現的位置
命令演示:
添加
setbit:簽到則為1,不簽到可以不輸入,默認為0
查看redis客戶端:
查詢
BITFIELD
在查詢時 offset指定從哪讀,type指定讀多少bit位,并且還要指定返回的是否帶符號。(因為返回的是十進制,因此要說明是否帶符號,如果帶符號,二進制第一位則為符號位,因此u代表無符號,i代表有符號,一般使用無符號)
舉例說明:
BITPOS
簽到功能
案例實現:簽到功能
需求:實現簽到接口,將當前用戶當天簽到信息保存到redis中
接口請求解析:
說明 | |
---|---|
請求方式 | Post |
請求路徑 | /user/login |
請求參數 | 無 |
返回值 | 無 |
在請求解析中,我們發現請求參數與返回值都為空,這是因為我們簽到所需的用戶以及當天日期都可以在后端直接獲取,因此不需要前端傳參,也不需要返回值,但如果是補簽功能的話,就需要前端傳遞日期參數了
注意:因為BitMap底層是基于String數據結構,因此其操作也都被封裝在字符串相關操作中了。
key組成:用戶+日期(原因:簽到往往是以月為統計單位的,因此每個用戶每個月的簽到情況放在一個BitMap中,方便統計)
代碼實現:
controller層:
?@PostMapping("/sign")public Result sign(){return userService.sign();}
Service層:
?@Overridepublic Result sign() {//1.獲取當前登錄用戶Long id ?= UserHolder.getUser().getId();//2.獲取當前日期LocalDateTime now = LocalDateTime.now();//3.拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyy/MM"));String key = USER_SIGN_KEY + id + keySuffix;//4.獲取今天是本月的第幾天int dayOfMouth = now.getDayOfMonth();//5.寫入Redis,setbit key offset 1stringRedisTemplate.opsForValue().setBit(key,dayOfMouth-1,true);return Result.ok();}
運行效果:
至此簽到功能完成。
簽到統計
簽到統計有很多種:比如統計該月總簽到次數、該月截止今天的連續簽到次數等等,
那么什么叫做連續簽到天數呢?
從最后一次簽到開始向前統計,直到遇到第一次未簽到為止,計算的總的簽到次數,就是連續簽到天數。
那么如何使用Java代碼實現統計連續簽到天數?
方法1:給每個bit位拼接逗號,然后spit(0),最后一個數組長度就是連續天數,最長的數組就是最長連續天數
方法2:從最后一個比特位開始遍歷,并定義一個計數器,為1則加一,為0則終止。其中有些關鍵問題:
問題1:如何得到本月到今天為止的所有簽到數據?
在BitMap的指令中:bitfield可以獲取指定范圍內的所有簽到數據,而該指令需要兩個參數,一個是從哪開始,另一個是查多少。因為要得到本月到今天為止的所有簽到數據,因此起始腳標為0,而offset則為日期值,
由此得到指令:bitfield key get u[dayOfMonth] 0
問題2:如何從后往前的遍歷每一個bit位
解答:與1做與運算,就能得到最后一個比特位。隨后在右移一位,下一個bit位就成為了最后一個bit位,隨后同上操作,以此類推,便可以從后向前的遍歷每一個bit位。
至此,思路理順,付諸實踐
案例展示:實現簽到統計功能
需求:實現下面接口,統計當前用戶截止當前時間在本月的連續簽到天數
請求解析:
說明 | |
---|---|
請求方式 | GET |
請求路徑 | /user/sign/out |
請求參數 | 無 |
返回值 | 連續簽到天數 |
代碼實現:
Controller層:
?@GetMapping("/sign/count")public Result signCount(){return userService.signCount();}
Service層:
?public Result signCount() {//1.獲取當前登錄用戶Long id ?= UserHolder.getUser().getId();//2.獲取當前日期LocalDateTime now = LocalDateTime.now();//3.拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyy/MM"));String key = USER_SIGN_KEY + id + keySuffix;//4.獲取今天是本月的第幾天int dayOfMouth = now.getDayOfMonth();//5.獲取本月截止今天為止的所有的簽到記錄 返回的是一個十進制的數字 bitfield sign:1:2025/08 get u6 0List<Long> result = stringRedisTemplate.opsForValue().bitField(key,BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMouth)).valueAt(0));if (result == null || result.isEmpty()){return Result.ok(0);}Long number = result.get(0);if (number == null || number == 0){return Result.ok(0);}//6.循環遍歷int count = 0;while (true){//6.1讓這個數字與1做與運算,得到數字的最后一個bit位 //判斷bit位是否為0if ((number & 1) == 0) {//如果為0,說明未簽到,結束break;}else {//如果不為0,說明已簽到,計數器加一count++;}//把數字右移一位,拋棄最后一個bit位,繼續下一個bit位的判斷// 將number無符號右移一位,相當于將number除以2,并將結果賦值給numbernumber >>>= 1;}return Result.ok(count);}
效果展示:
至此用戶簽到功能完成
希望對大家有所幫助