学 无 止 境 , 与 君 共 勉 。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上,方便你们访问
创做不易,若是各位以为有帮助,求点赞 支持
微信公众号: 俞大仙