Bitmaps并不属于Redis中数据结构的一种,它的命令基于String操作,是set
、get
等一系列字符串操作的一种扩展,与其不同的是,它提供的是位级别的操作,从这个角度看,我们也可以把它当成是一种位数组、位向量结构。当我们需要存取一些boolean类型的信息时,Bitmap是一个非常不错的选择,在节省内存的同时也拥有很好的存取速度(getbit/setbit操作时间复杂度为O(1))。
1 常用命令
SETBIT key offset value
设置或者清空key的value(字符串)在offset处的bit值。当key不存在的时候,将新建字符串value。参数offset需要大于等于0,并且小于232(限制Bitmap大小为512MB)。当key对应的字符串增大的时候,新增的部分bit值都是设置为0。
GETBIT key offset
返回key对应的string在offset处的bit值。当offset超出了字符串长度或key不存在时,返回0。
BITCOUNT key [start end]
统计字符串被设置为1的bit数。需要注意的是,这里的start和end并不是位偏移,而是以字节(8位)为单位来偏移的,比如BITCOUNT foo 0 1
是统计key为foo的字符串中第一个到第二个字节中bit为1的总数。
2 使用Bitmaps实现用户签到
Bitmap常见的应用场景之一就是用户签到了,在这里,我们以日期作为key,以用户ID作为位偏移,存储用户的签到信息(1为签到,0为未签到)。
其实现如下(Spring Boot):
public class CheckInService {
private static final String CHECK_IN_PRE_KEY = "USER_CHECK_IN::DAY::";
private static final String CONTINUOUS_CHECK_IN_COUNT_PRE_KEY = "USER_CHECK_IN::CONTINUOUS_COUNT::";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 用户签到
*
* @param userId 用户ID
*/
public void checkIn(Long userId) {
String today = LocalDate.now().format(DATE_TIME_FORMATTER);
if(isCheckIn(userId, today))
return;
stringRedisTemplate.opsForValue().setBit(getCheckInKey(today), userId, true);
updateContinuousCheckIn(userId);
}
/**
* 检查用户是否签到
*
* @param userId
* @param date
* @return
*/
public boolean isCheckIn(Long userId, String date) {
Boolean isCheckIn = stringRedisTemplate.opsForValue().getBit(getCheckInKey(date), userId);
return Optional.ofNullable(isCheckIn).orElse(false);
}
/**
* 统计特定日期签到总人数
*
* @param date
* @return
*/
public Long countDateCheckIn(String date) {
byte [] key = getCheckInKey(date).getBytes();
Long result = stringRedisTemplate.execute(new RedisCallback<Long>() {
@Nullable
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
return connection.bitCount(key);
}
});
return Optional.ofNullable(result).orElse(0L);
}
/**
* 获取用户某个时间段签到次数
*
* @param userId
* @param startDate
* @param endDate
* @return
*/
public Long countCheckIn(Long userId, String startDate, String endDate) {
LocalDate startLocalDate = LocalDate.parse(startDate, DATE_TIME_FORMATTER);
LocalDate endLocalDate = LocalDate.parse(endDate, DATE_TIME_FORMATTER);
AtomicLong count = new AtomicLong(0);
long distance = Period.between(startLocalDate, endLocalDate).get(ChronoUnit.DAYS);
if(distance < 0)
return count.get();
Stream.iterate(startLocalDate, d -> d.plusDays(1)).limit(distance + 1).forEach((LocalDate date) -> {
Boolean isCheckIn = stringRedisTemplate.opsForValue().
getBit(getCheckInKey(date.format(DATE_TIME_FORMATTER)), userId);
if(isCheckIn)
count.incrementAndGet();
});
return count.get();
}
/**
* 更新用户连续签到天数:+1
* @param userId
*/
public void updateContinuousCheckIn(Long userId) {
String key = getContinuousCheckInKey(userId);
String val = stringRedisTemplate.opsForValue().get(key);
long count = 0;
if(val != null){
count = Long.parseLong(val);
}
count ++;
stringRedisTemplate.opsForValue().set(key, String.valueOf(count));
//设置第二天过期
stringRedisTemplate.execute(new RedisCallback<Void>() {
@Nullable
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
LocalDateTime dateTime = LocalDateTime.now().plusDays(2).withHour(0).withMinute(0).withSecond(0);
connection.expireAt(key.getBytes(), dateTime.toInstant(ZoneOffset.of("+8")).getEpochSecond());
return null;
}
});
}
/**
* 获取用户连续签到天数
* @param userId
* @return
*/
public Long getContinuousCheckIn(Long userId) {
String key = getContinuousCheckInKey(userId);
String val = stringRedisTemplate.opsForValue().get(key);
if(val == null){
return 0L;
}
return Long.parseLong(val);
}
private String getCheckInKey(String date) {
return CHECK_IN_PRE_KEY + date;
}
private String getContinuousCheckInKey(Long userId) {
return CONTINUOUS_CHECK_IN_COUNT_PRE_KEY + userId;
}
}