Redis实现签到功能

使用 Redisson + BitMap 实现签到

1、引入 Redisson 依赖

pom.xml

        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

2、配置 Redisson

@Configuration
public class RedissonConfig {

    @Bean
    public Redisson redisson() {
        // 此为单机模式
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

3、签到功能

数据库

CREATE TABLE `user_integral` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
  `integral` int(11) DEFAULT NULL COMMENT '用户积分',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE `user_integral_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
  `integral_type` int(1) DEFAULT NULL COMMENT '积分类型:1.正常签到  2.连续签到获赠积分',
  `integral` int(11) DEFAULT NULL COMMENT '积分',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

常量

public interface RedisKeyConstant {

    /**
     * 用户签到 Redis key 前缀
     */
    String USER_SIGN_IN = "userSign";

    /**
     * 连续签到积分奖励配置,后续改为从数据库中获取
     */
    Map<Integer, Integer> SIGN_CONFIGURATION = new HashMap<>() {{
        put(5, 10);
        put(7, 30);
        put(15, 80);
        put(32, 150);
    }};

    /**
     * 用户普通签到所获取积分
     */
    Integer SIGN_TYPE_NORMAL_INTEGRAL = 5;
}

服务类

下边实现了签到功能,并且可以获得连续签到天数

补签功能目前没有实现,不过也不难,在补签的接口中,只需要拿到 userId 和 day(当前日期) 即可,这样在 BitMap 中,把对应日期的 bit 设置为 1,再扣除补签所对应的代价即可。

@Service
@Slf4j
public class UserIntegralServiceImpl extends ServiceImpl<UserIntegralMapper, UserIntegral> implements UserIntegralService {

    @Autowired
    Redisson redisson;

    @Autowired
    private UserIntegralLogService userIntegralLogService;

    @Override
    public ResultBody sign(Integer userId) {
        LocalDateTime now = LocalDateTime.now();
        int year = now.getYear();
        int month = now.getMonthValue();
        int day = now.getDayOfMonth();
        // usersign key 为 usersign:[userId][year][month]
        RBitSet userSign = redisson.getBitSet(RedisKeyConstant.USER_SIGN_IN + userId + year + month);
        // 进行签到
        userSign.set(day);
        log.info("{} 签到成功,key 为:{}", userId, RedisKeyConstant.USER_SIGN_IN + userId + year + month);
        BitSet bs = userSign.asBitSet();
        // 用户所得积分
        int count = 0;

        // 每次签到获得基础分数
        count += RedisKeyConstant.SIGN_TYPE_NORMAL_INTEGRAL;

        List<UserIntegralLog> userIntegralLogs = new ArrayList<>();
        // 记录正常签到日志
        UserIntegralLog userIntegralLog = new UserIntegralLog();
        userIntegralLog.setIntegral(count);
        userIntegralLog.setUserId(userId);
        userIntegralLog.setIntegralType(UserSignTypeEnums.SIGN_NORMAL_INTEGRAL.getType());
        userIntegralLog.setCreateTime(LocalDateTime.now());

        userIntegralLogs.add(userIntegralLog);

        int continousSignDays = getContinousSignDays(bs);
        // 如果连续签到的天数等于当月的总天数的话,表明整月全部签到,用连续签到 32 天来标记
        if (continousSignDays == LocalDate.now().lengthOfMonth()) {
            continousSignDays = 32;
        }
      
        // 获取连续签到奖励配置
        Map<Integer, Integer> signConfiguration = RedisKeyConstant.SIGN_CONFIGURATION;

        /**
         * 获取当前连续签到天数所对应的奖励,只有连续签到指定天数才会获得奖励
         * 如果对应的连续签到天数没有奖励,那么获取的 continousIntegral 为 null
         */
        Integer continousIntegral = signConfiguration.get(continousSignDays);
        if (continousIntegral != null) {
            // 如果是整月都签到,这里将连续签到天数设置为准确的该月天数
            if (continousSignDays == 32) {
                continousSignDays = LocalDate.now().lengthOfMonth();
            }
            // 记录连续签到获得奖励日志
            UserIntegralLog continousIntegralLog = new UserIntegralLog();
            continousIntegralLog.setIntegral(continousIntegral);
            continousIntegralLog.setUserId(userId);
            continousIntegralLog.setIntegralType(UserSignTypeEnums.SIGN_CONTINOUS_INTEGRAL.getType());
            continousIntegralLog.setCreateTime(LocalDateTime.now());
            userIntegralLogs.add(continousIntegralLog);
            // count 为用户本次签到所得积分
            count += continousIntegral;
        }

        boolean flag = updateUserIntegral(userId, count) && userIntegralLogService.saveBatch(userIntegralLogs);

        return flag ? ResultBody.success("签到成功") : ResultBody.error("签到失败");
    }

    private boolean updateUserIntegral(Integer userId, int count) {
        QueryWrapper<UserIntegral> qw = new QueryWrapper<>();
        qw.eq("user_id", userId);
        UserIntegral userIntegral = this.getBaseMapper().selectOne(qw);
        // 如果之前没有记录用户积分,这里记录一下
        if (userIntegral == null) {
            userIntegral = new UserIntegral();
            userIntegral.setUserId(userId);
            userIntegral.setIntegral(0);
            userIntegral.setCreateTime(LocalDateTime.now());
            this.getBaseMapper().insert(userIntegral);
        }
        userIntegral.setIntegral(userIntegral.getIntegral() + count);
        int i = this.getBaseMapper().updateById(userIntegral);
        return i > 0;
    }


    @Override
    public Map<Integer, Boolean> getSignMap(Integer userId) {
        LocalDateTime now = LocalDateTime.now();
        int year = now.getYear();
        int month = now.getMonthValue();
        int day = now.getDayOfMonth();
        RBitSet userSign = redisson.getBitSet(String.format(RedisKeyConstant.USER_SIGN_IN, month, day));
        BitSet bs = userSign.asBitSet();
        Map<Integer, Boolean> map = new HashMap<>();
        for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i+1)) {
            // operate on index i here
            if (i == Integer.MAX_VALUE) {
                break; // or (i+1) would overflow
            }
            map.put(i, true);
        }
        /**
         * 先通过 Map 存储连续签到对应的积分
         * 1. 通过连续签到天数拿到对应需要得到的积分
         * 2. 日志记录积分
         */
        return map;
    }

    public int getContinousSignDays(BitSet bs) {
        List<Integer> list = new ArrayList<>();
        for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i+1)) {
            // operate on index i here
            if (i == Integer.MAX_VALUE) {
                break; // or (i+1) would overflow
            }
            list.add(i);
        }
        int day = LocalDateTime.now().getDayOfMonth();
        int lastDay = day;
        int continousSignDay = 0;
        if (list.get(list.size() - 1) == day) {
            for (int i = list.size() - 1; i >= 0; i --) {
                if (list.get(i) == lastDay) {
                    continousSignDay ++;
                    lastDay --;
                } else {
                    break;
                }
            }
        }
        return continousSignDay;
    }


}

Redis 原生实现签到

  • 签到

我们先来设计 key:userid:yyyyMM,那么假如 usera 在2023年10月3日和2023年10月4日签到的话,使用以下命令:

setbit key offset value

在3日签到的话,偏移量应该设置为2,则签到之后为 001

在4日签到的话,偏移量应该设置为3,则3日、4日签到后为0011(第3位和第4位都是1,表示这两天签到了)

在31日签到的话,偏移量应该设置为30,表示向右偏移30位

127.0.0.1:6379> setbit userx:202310 2 1
(integer) 0
127.0.0.1:6379> setbit userx:202310 3 1
(integer) 0
127.0.0.1:6379> setbit userx:202310 30 1
(integer) 0
  • 查看2023年10月哪些天签到了,10月有31天,所以使用u31(31位无符号整数),后边偏移量为0
127.0.0.1:6379> bitfield userx:202310 get u31 0
1) (integer) 402653185

通过 bitfield [key] get u31 0 获取了31位的无符号整数,将该整数402653185转为二进制如下:(可以发现从左向右第4位和第5位位1,表示这两天签到了,也就是3号签到的时候,在setbit key value offset中,偏移量设置了为3,所以向右偏移3位,在第4位上)

402653185 十进制
0011000000000000000000000000001 二进制

通过取出来这个无符号整数,我们在代码中就可以通过位运算来判断某一天是否签到,可以看出,第3、4、31位都是1,表明在这三天都进行了签到

那么如果今天是10月26日,我们想知道10月1日-10月26日有哪些天进行签到,使用如下命令:

127.0.0.1:6379> bitfield userx:202310 get u26 0
1) (integer) 12582912

12582912 转为二进制为:

26位无符号整数:
00110000000000000000000000

那么我们在计算连续签到的次数就可以使用以下方法:

public Integer getContinuousSignCount() {
  /*
  假如今天是 10月26日
  假如通过 redis 的 bitfield userx:202310 get u26 0 命令得到了从1日-26日的签到数据为:12582912
  */
  Long v = 12582912;
  // 这里 26 为今天的日期
  Integer signCount = 0;
  for (int i = 0; i < 26; i ++) {
    if (v & 1 == 0) return signCount;
    signCount ++;
    v >>= 1;
  }
}

你可能感兴趣的:(面试题,redis,数据库,缓存)