學 無 止 境 , 與 君 共 勉 。php
按照官網的說法,Redis位圖Bitmaps不是實際的數據類型,而是在字符串類型上定義的一組面向位的操做。在Redis中字符串限制最大爲512MB,因此位圖中最大能夠設置2^32個不一樣的位(42.9億個)。圖位的最小單位是比特(bit),每一個bit的值只能是0或1。css
位圖的存儲大小計算: (maxOffset / 8 / 1024 / 1024)MB。其中maxOffset爲位圖的最大位數java
設置指定key的值在offset處的bit值,offset從0開始。返回值爲在offset處原來的bit值git
# 經過位操做將 h 改爲 i
127.0.0.1:6379> SET h h # 二進制爲 01101000
OK
127.0.0.1:6379> SETBIT h 7 1 # 將最後一位改爲1 => 01101001
(integer) 0
127.0.0.1:6379> GET h
"i"
複製代碼
獲取指定key的值在offset處的bit值,offset從0開始。若是offset超出了當前位圖的範圍,則返回0。github
127.0.0.1:6379> set i i # 二進制爲 01101001
OK
127.0.0.1:6379> getbit i 0 # 第1位爲0
(integer) 0
127.0.0.1:6379> getbit i 1 # 第2位爲0
(integer) 1
127.0.0.1:6379> getbit i 7 # 第8位爲0
(integer) 1
複製代碼
統計指定key值中被設置爲1的bit數。能夠經過指定參數star和end來限制統計範圍。web
注意,這裏的star和end不是指bit的下標,而是字節(byte)的下標。好比start爲1,則實際對應的bit下標爲8(1byte = 8 bit)redis
127.0.0.1:6379> set hi hi # 二進制爲 0110100001101001
OK
127.0.0.1:6379> bitcount hi # 全部是1的位數:7個
(integer) 7
127.0.0.1:6379> bitcount hi 1 2 # 即統計 01101001 中1的位數
(integer) 4
複製代碼
統計首次出現的0或1的bit位,能夠經過start和end來指定範圍,一樣是指字節的下標。數據庫
127.0.0.1:6379> get nilkey # 不存在的key
(nil)
127.0.0.1:6379> bitpos nilkey 1 # 在不存在的key中查首次出現1的位
(integer) -1
127.0.0.1:6379> setbit nilkey 0 0 # 空字符串
(integer) 0
127.0.0.1:6379> get nilkey
"\x00"
127.0.0.1:6379> bitpos nilkey 1
(integer) -1
複製代碼
對一個或多個二進制位字符串進行操做,並將結果保存到 destkey 上。當某個字符串長度不夠時,對應的位用0補上ruby
127.0.0.1:6379> set a a # 二進制 01100001
OK
127.0.0.1:6379> set c c # 二進制 01100011
OK
127.0.0.1:6379> bitop and destkey a c # 與操做 01100001 -> a
(integer) 1
127.0.0.1:6379> get destkey
"a"
複製代碼
127.0.0.1:6379> set a a # 二進制 01100001
OK
127.0.0.1:6379> set b b # 二進制 01100010
OK
127.0.0.1:6379> bitop or destkey a b # 或操做 01100011 -> c
(integer) 1
127.0.0.1:6379> get destkey
"c"
127.0.0.1:6379>
複製代碼
127.0.0.1:6379> set a a # 二進制 01100001
OK
127.0.0.1:6379> set z Z # 二進制 01011010 (大寫的Z)
OK
127.0.0.1:6379> bitop xor destkey a z # 異或 00111011 -> ; 分號
(integer) 1
127.0.0.1:6379> get destkey
";"
複製代碼
01010101 -> 10101010
複製代碼
這裏用一個用戶簽到的例子來說解如何在實戰中應用,需求:微信
使用位圖的好處:
這裏基於SpringBoot進行演示:
sign:{yyyyMMdd}
將用戶ID做爲偏移量,經過setBit
設置該位置的值爲1
將用戶ID做爲偏移量,經過getBit
查詢該位置上的值是否爲1
經過bitCount
去實現統計
Redis中並無提供對多個二進制位字符串進行求和操做,咱們須要本身去統計。思路:
sign:2020
開頭的key,能夠經過Redis指令keys sign:2020*
獲取邏輯與
操做,生成一個連續七天簽到的記錄和統計7天連續簽到思路同樣,只是這裏使用邏輯或
操做
@Service
public class RedisService {
private final StringRedisTemplate stringRedisTemplate;
public RedisService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 獲取指定格式的key
*
* @param pattern 格式
* @return set
*/
public Set<String> getKeys(String pattern) {
return stringRedisTemplate.keys(pattern);
}
/**
* 設置指定位的值
*
* @param key 鍵
* @param offset 偏移量 0開始 對應bit的位置
* @param value true爲1,false爲0
* @return boolean
*/
public Boolean setBit(String key, long offset, boolean value) {
return stringRedisTemplate.opsForValue().setBit(key, offset, value);
}
/**
* 獲取指定位的值
*
* @param key 鍵
* @param offset 偏移量 0開始
* @return boolean
*/
public Boolean getBit(String key, long offset) {
return stringRedisTemplate.opsForValue().getBit(key, offset);
}
/**
* 統計字符串被設置爲1的bit數
*
* @param key 鍵
* @return long
*/
public Long bitCount(String key) {
return stringRedisTemplate.execute(
(RedisCallback<Long>) connection -> connection.bitCount(key.getBytes())
);
}
/**
* 統計字符串指定位上被設置爲1的bit數
*
* @param key 鍵
* @param start 開始位置 注意對應byte的位置,是bit位置*8
* @param end 結束位置
* @return long
*/
public Long bitCount(String key, long start, long end) {
return stringRedisTemplate.execute(
(RedisCallback<Long>) connection -> connection.bitCount(key.getBytes(), start, end)
);
}
/**
* 不一樣字符串之間進行位操做
*
* @param op 操做類型:與、或、異或、否
* @param destKey 最終存放結構的鍵
* @param keys 要操做的鍵
* @return Long
*/
public Long bitOp(RedisStringCommands.BitOperation op, String destKey, Collection<String> keys) {
int size = keys.size();
byte[][] bytes = new byte[size][];
int index = 0;
for (String key : keys) {
bytes[index++] = key.getBytes();
}
return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitOp(op, destKey.getBytes(), bytes));
}
/**
* 對符合指定格式的key值進行未操做
*
* @param op 操做類型:與、或、異或、否
* @param destKey 存放結果的鍵
* @param pattern key格式
* @return Long
*/
public Long bitOp(RedisStringCommands.BitOperation op, String destKey, String pattern) {
Set<String> keys = getKeys(pattern);
int size = keys.size();
if (size == 0) {
return 0L;
}
byte[][] bytes = new byte[size][];
int index = 0;
for (String key : keys) {
bytes[index++] = key.getBytes();
}
return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitOp(op, destKey.getBytes(), bytes));
}
}
複製代碼
@RestController
@RequestMapping("/redis/bit")
public class BitMapController {
private final DateTimeFormatter formatters = DateTimeFormatter.ofPattern("yyyyMMdd");
/**
* 定義簽到前綴
* key格式爲 sing:{yyyyMMdd}
*/
private static final String SIGN_PREFIX = "sign:";
/**
* 連續一週簽到
*/
private static final String SIGN_ALL_WEEK_KEY = "signAllWeek";
/**
* 連續一個月簽到
*/
private static final String SIGN_ALL_MONTH_KEY = "signAllMonth";
/**
* 一週內有簽到過的
*/
private static final String SIGN_IN_WEEK_KEY = "signInWeek";
private final RedisService redisService;
public BitMapController(RedisService redisService) {
this.redisService = redisService;
}
/**
* 初始化本年今天以前的測試數據
*/
@GetMapping("/init")
public void initData() {
// 獲取本年的日期列表
List<String> dateKeyList = new ArrayList<>();
LocalDate curDate = LocalDate.now();
LocalDate beginDate = LocalDate.parse("2020-01-01");
while (beginDate.isBefore(curDate)) {
dateKeyList.add(SIGN_PREFIX + beginDate.format(formatters));
beginDate = beginDate.plusDays(1);
}
// 是否簽到
boolean isSign;
StringBuilder signInfo;
for (int i = 1; i < 6; i++) {
signInfo = new StringBuilder("用戶【").append(i).append("】:");
for (String dateKey : dateKeyList) {
if (i == 1) {
// 用戶1所有簽到
isSign = true;
} else {
// 其餘用戶隨機
isSign = Math.random() > 0.5;
}
redisService.setBit(dateKey, i, isSign);
signInfo.append(isSign ? 1 : 0).append(", ");
}
System.out.println(signInfo.toString());
}
}
/**
* 用戶當天簽到
* 用戶ID做爲位圖的偏移量
*/
@GetMapping("/sign/{userId}")
public String sign(@PathVariable Long userId) {
redisService.setBit(SIGN_PREFIX + getCurDate(), userId, true);
return "簽到成功";
}
/**
* 查詢用戶今天是否已經簽到了
*/
@GetMapping("/isSign/{userId}")
public String isSign(@PathVariable Long userId) {
Boolean isSign = redisService.getBit(SIGN_PREFIX + getCurDate(), userId);
if (isSign) {
return String.format("用戶【%d】今日已簽到", userId);
}
return String.format("用戶【%d】今日還沒有簽到,請簽到", userId);
}
/**
* 統計今天全部的簽到數量
*/
@GetMapping("/todayCount")
public String todayCount() {
return String.format("今日已簽到人數: %d", redisService.bitCount(SIGN_PREFIX + getCurDate()));
}
/**
* 統計指定用戶整年的簽到數
*/
@GetMapping("/userYearSign/{userId}")
public String userYearSign(@PathVariable Long userId) {
int year = LocalDate.now().getYear();
// 獲取全部的key
Set<String> keys = redisService.getKeys(SIGN_PREFIX + year + "*");
/*
* 可使用BitSet 去存儲用戶天天的簽到信息,用於其餘的操做
* BitSet users = new BitSet();
* 統計全部已經簽到的數量 對應 redis的bitCount
* users.cardinality()
*/
int signCount = 0;
for (String key : keys) {
if (redisService.getBit(key, userId)) {
signCount++;
}
}
return String.format("本年已累計簽到: %d 次", signCount);
}
/**
* 統計近7天連續簽到的用戶數量
* 邏輯與
*/
@GetMapping("/signAllWeek")
public String signAllWeek() {
List<String> weekDays = getWeekKeys();
redisService.bitOp(RedisStringCommands.BitOperation.AND, SIGN_ALL_WEEK_KEY, weekDays);
return String.format("近7天連續簽到用戶數:%d", redisService.bitCount(SIGN_ALL_WEEK_KEY));
}
/**
* 統計本月所有簽到過的用戶數量
*/
@GetMapping("/signAllMonth")
public String signAllMonth() {
redisService.bitOp(
RedisStringCommands.BitOperation.AND,
SIGN_ALL_MONTH_KEY,
SIGN_PREFIX + LocalDate.now().getYear()
);
return String.format("月所有簽到過的用戶數:%d", redisService.bitCount(SIGN_ALL_MONTH_KEY));
}
/**
* 統計近7天有過簽到的用戶數量,只簽到1次也算
* 邏輯或
*/
@GetMapping("/signInWeek")
public String signInWeek() {
List<String> weekDays = getWeekKeys();
redisService.bitOp(RedisStringCommands.BitOperation.OR, SIGN_IN_WEEK_KEY, weekDays);
return String.format("近7天有過簽到的用戶數:%d", redisService.bitCount(SIGN_IN_WEEK_KEY));
}
/**
* 獲取當天的日期
*
* @return yyyyMMdd
*/
private String getCurDate() {
return LocalDate.now().format(formatters);
}
/**
* 獲取近一週的日期對應的key
*/
private List<String> getWeekKeys() {
List<String> dateList = new ArrayList<>();
LocalDate curDate = LocalDate.now();
dateList.add(SIGN_PREFIX + curDate.format(formatters));
for (int i = 1; i < 7; i++) {
dateList.add(SIGN_PREFIX + curDate.plusDays(-i).format(formatters));
}
return dateList;
}
}
複製代碼
BITOP
操做時會從新生成一個結果的key,能夠在天天凌晨經過定時任務去統計以前的記錄來生成這個結果key,這樣在業務中就能夠直接經過這個結果key來統計數據全部代碼均上傳至Github上,方便你們訪問
創做不易,若是各位以爲有幫助,求點贊 支持
微信公衆號: 俞大仙