redis Bitmap
位图本质是数组,它基于string数据类型的按位操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量(可以成为一个索引或者位格)。Bitmap支持的最大位数是232位,它可以极大的节省存储空间,使用512M内存就可以存储42.9亿的字节信息(232=4294967296)。实际应用场景:用户签到、用户在线状态、统计活跃用户、自定义布隆过滤器、点赞功能。
用户签到demo
引入pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis配置
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 10
lettuce:
pool:
max-active: 100 # 连接池最大连接数(使用负值表示没有限制) 默认 8
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-idle: 10 # 连接池中的最大空闲连接 默认 8
min-idle: 4 # 连接池中的最小空闲连接 默认 0
timeout: 3000 # 连接超时时间(毫秒)
创建bean
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
签到类
@Service
@Slf4j
public class RedisBitService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean setBit(final String key, long offset, boolean value) {
return redisTemplate.opsForValue().setBit(key, offset, value);
}
public boolean getBit(final String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
public Long getBitCount(final String key) {
return redisTemplate.execute((RedisCallback<Long>) connection -> {
byte[] keybytes = redisTemplate.getStringSerializer().serialize(key);
return connection.bitCount(keybytes);
});
}
/**
* bitpos key中第一个有标记的位置
*
* @param key
* @param value
* @return
*/
public Long bitpos(final String key, boolean value) {
return redisTemplate.execute((RedisCallback<Long>) connection -> {
byte[] keybytes = redisTemplate.getStringSerializer().serialize(key);
return connection.bitPos(keybytes, value);
});
}
/**
* 获取一段位移量的位移值(返回结果为十进制数,转二进制即一段位移内的标记情况)
*/
public List<Long> bitField(final String key, int limit, int offset) {
return redisTemplate.execute((RedisCallback<List<Long>>) connection -> {
byte[] keybytes = redisTemplate.getStringSerializer().serialize(key);
return connection.bitField(keybytes, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(limit)).valueAt(offset));
});
}
/**
* 用户签到
*
* @param uid 用户ID
* @param date 日期
* @return 之前的签到状态
*/
public boolean doSign(int uid, LocalDate date) {
int offset = date.getDayOfMonth() - 1;
return setBit(buildSignKey(uid, date), offset, true);
}
/**
* 用户签到
*
* @param uid
* @param date
* @param offset
* @return
*/
public boolean donoSign(int uid, LocalDate date, int offset) {
return setBit(buildSignKey(uid, date), offset, false);
}
/**
* 用户签到
*
* @param uid
* @param date
* @param offset
* @return
*/
public boolean doSign(int uid, LocalDate date, int offset) {
return setBit(buildSignKey(uid, date), offset, true);
}
/**
* 检查用户是否签到
*
* @param uid 用户ID
* @param date 日期
* @return 当前的签到状态
*/
public boolean checkSign(int uid, LocalDate date) {
int offset = date.getDayOfMonth() - 1;
return getBit(buildSignKey(uid, date), offset);
}
/**
* 获取用户签到次数
*
* @param uid 用户ID
* @param date 日期
* @return 当前的签到次数
*/
public long getSignCount(int uid, LocalDate date) {
return getBitCount(buildSignKey(uid, date));
}
/**
* 获取当月连续签到次数
*
* @param uid 用户ID
* @param date 日期
* @return 当月连续签到次数
*/
public long getContinuousSignCount(int uid, LocalDate date) {
int signCount = 0;
int max = 0;
BitFieldSubCommands subCommands = BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(date.lengthOfMonth())).valueAt(0);
List<Long> list = redisTemplate.opsForValue().bitField(buildSignKey(uid, date), subCommands);
if (list != null && list.size() > 0) {
// 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = 0; i < date.lengthOfMonth(); i++) {
if (v >> 1 << 1 == v) {
// 低位为0且非当天说明连续签到中断了
if (i > 0) {
if (max < signCount) {
max = signCount;
}
signCount = 0;
}
} else {
signCount += 1;
}
v >>= 1;
}
}
return max;
}
/**
* 获取当月首次签到日期
*
* @param uid 用户ID
* @param date 日期
* @return 首次签到日期
*/
// public LocalDate getFirstSignDate(int uid, LocalDate date) {
// long pos = redisTemplate.opsForValue().bitPos(buildSignKey(uid, date), true);
// return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1));
// }
/**
* 获取当月签到情况
*
* @param uid 用户ID
* @param date 日期
* @return Key为签到日期,Value为签到状态的Map
*/
public Map<String, Boolean> getSignInfo(int uid, LocalDate date) {
Map<String, Boolean> signMap = new LinkedHashMap<>();
BitFieldSubCommands subCommands = BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(date.lengthOfMonth())).valueAt(1);
List<Long> list = redisTemplate.opsForValue().bitField(buildSignKey(uid, date), subCommands);
if (list != null && list.size() > 0) {
// 由低位到高位,为0表示未签,为1表示已签例如: (10000111111111) 第一天的在第一位
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = date.lengthOfMonth(); i > 0; i--) {
LocalDate d = date.withDayOfMonth(i);
//向右移一位再向左移一位 若前后的值相等则表示最后一位为0 否则是1
signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
v >>= 1;
}
}
return signMap;
}
private static String formatDate(LocalDate date) {
return formatDate(date, "yyyyMM");
}
private static String formatDate(LocalDate date, String pattern) {
return date.format(DateTimeFormatter.ofPattern(pattern));
}
private static String buildSignKey(int uid, LocalDate date) {
return String.format("u:sign:%d:%s", uid, formatDate(date));
}
}
测试
@Autowired
private RedisBitService bitService;
/**
* 测试签到功能
*
* @return
*/
@GetMapping("/bit")
public Object test() {
LocalDate today = LocalDate.now();
int uid = 1000;
for (int i = 1; i < 32; i++) {
bitService.donoSign(1000, today, i);
}
for (int i = 1; i < 32; i++) {
System.out.println(today.withDayOfMonth(i));
if (i == 1 || (i > 7 && i < 15)) {
bitService.doSign(1000, today, i);
}
if (i > 15 && i % 2 == 0) {
bitService.doSign(1000, today, i);
}
}
boolean state = bitService.checkSign(uid, today);
log.info("userSignDemo.checkSign:{}", state);
// LocalDate localDate = bitService.getFirstSignDate(uid, today);
// System.out.println("userSignDemo.getFirstSignDate:" + localDate);
Long count = bitService.getSignCount(uid, today);
log.info("userSignDemo.getSignCount:{}", count);
Long continuousSignCount = bitService.getContinuousSignCount(uid, today);
log.info("userSignDemo.continuousSignCount:{}", continuousSignCount);
Map<String, Boolean> map = bitService.getSignInfo(uid, today);
for (Map.Entry<String, Boolean> entry : map.entrySet()) {
log.info(entry.getKey() + ": {}", (entry.getValue() ? "√" : "-"));
}
return true;
}