Redis中Bitmaps的使用(签到功能的实现)

Bitmaps并不属于Redis中数据结构的一种,它的命令基于String操作,是setget等一系列字符串操作的一种扩展,与其不同的是,它提供的是位级别的操作,从这个角度看,我们也可以把它当成是一种位数组、位向量结构。当我们需要存取一些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() {
            @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() {
            @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;
    }
}

完整代码

你可能感兴趣的:(Redis)