Redis实战--基于Bitmap实现用户签到功能

需求:

1)签到1天得1积分,连续签到2天得2积分,3天得3积分,3天以上均得3积分

2)如果连续签到中断,则重置计数,并且每月重置计数

3)显示用户某月的签到次数

4)在日历控件上展示用户每月签到,可以切换年月显示

对于用户签到数据,如果直接采用数据库存储,当出现高并发访问时,对数据库压力会很大。所以采用缓存来减轻数据库的压力

Redis提供的数据类型BitMap(位图),每个bit对应0/1状态,BitMap的内部是采用String存储,但是提供了一些指令对BitMap直接操作

常见指令:

实现方式:

用户每月存储一条签到数据。key的格式为u:sign:{uid}:{yyyMM} ,而value采用长度为4个字节(32bit)的BitMap,BitMap的每一位代表每一天的签到,1表示已签,0表示未签

例如:

SETBIT u:sign:1225:202403 5 1 #代表ID为1225的用户在2024年3月的签到记录 5 1 表示3月6号签到(因为偏移量是从0开始,所以把6减1)

GETBIT u:sign:1225:202403 5 #检查用户在3月6号的签到结果

BITCOUNT u:sign:1225:202403 #统计2024年3月的签到次数

BITPOS u:sign:1225:202403 1 #返回3月首次签到的日期

BITFIELD u:sign:1225:202403 get u31 0 #获取3月份前31天得签到数据

实例代码:

import java.util.Date;
import java.util.Map;

public interface UserSignService {
    //用户签到
    boolean doSign(int uid,Date date);

    //检查用户是否签到
    boolean checkSign(int uid,Date date);

    //获取用户签到次数
    long getSignCount(int uid,Date date);

    //获取当月连续签到次数
    long GetContinuousSignCount(int uid,Date date);

    //获取当月首次签到日期
    Date getFirstSignDate(int uid,Date date);

    //获取本月签到情况
    Map<String,Boolean> getSignInfo(int uid,Date date);
}
package com.yxy.pet.service.impl;

import com.sun.tools.javac.util.List;
import com.yxy.pet.service.UserSignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.redis.connection.BitFieldSubCommands;
@Service
public class UserSignServiceImpl implements UserSignService {
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Override
    public boolean doSign(int uid, Date date) {
        int offset=date.getDay()-1;
        String key=buildSignKey(uid,date);
        return redisTemplate.opsForValue().setBit(key,offset,true);
    }

    @Override
    public boolean checkSign(int uid, Date date) {
        int offset=date.getDay()-1;
        String key=buildSignKey(uid,date);
        return redisTemplate.opsForValue().getBit(key,offset);
    }

    @Override
    public long getSignCount(int uid, Date date) {
        String key=buildSignKey(uid,date);

        Long bitCount = (Long) redisTemplate.execute((RedisCallback<Long>) connection -> {
            return connection.bitCount(key.getBytes());
        });

        return bitCount != null ? bitCount : 0;
    }

    private String buildSignKey(int uid, Date date) {
        return "u:sign:"+uid+":"+ FormatDate(date,"yyyyMM");
    }
    private static String FormatDate(Date date, String pattern) {
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        return sdf.format(date);
    }


    @Override
    public long GetContinuousSignCount(int uid, Date date) {
        String key=buildSignKey(uid,date);
        int signCount = 0;
        String type = "u" + date.getDay(); // 取1号到当天的签到状态

        //通过redisTemplate执行bitField命令,获取存储在redis位图中制定偏移量的值
        Object result = redisTemplate.execute((RedisCallback<Long>) connection ->
                connection.bitField(key.getBytes(), BitFieldSubCommands.get(BitFieldSubCommands.BitFieldType.unsigned(date.getDay())))
        );

        //通过位运算检查每一天的签到状态,计算连续签到的天数
        if (result != null) {
            long[] list = (long[]) result;
            if (list.length > 0) {
                long v = list[0];
                for (int i = 0; i < date.getDay(); i++) {
                    if ((v >> 1) << 1 == v) {
                        if (i > 0) break;
                    } else {
                        signCount += 1;
                    }
                    v >>= 1;
                }
            }
        }
        return signCount;
    }


    @Override
    public Date getFirstSignDate(int uid, Date date) {
        String key=buildSignKey(uid,date);
        Long pos = redisTemplate.execute((RedisCallback<Long>) connection ->
                connection.bitPos(key.getBytes(), true));
        return pos < 0 ? null : new Date(date.getTime() - (date.getDay() - pos + 1) * 24 * 60 * 60 * 1000);
    }

    @Override
    public Map<String, Boolean> getSignInfo(int uid, Date date) {
        String key=buildSignKey(uid,date);
        Map<String, Boolean> signMap = new HashMap<>();
        String type = "u" + getDayOfMonth(date);
        Object result = redisTemplate.execute((RedisCallback<Object>) connection ->
                connection.bitField(key.getBytes(), BitFieldSubCommands.get().unsigned(getDayOfMonth(date)))
        );
        if (result != null) {
            long[] list = (long[]) result;
            if (list.length > 0) {
                long v = list[0];
                for (int i = getDayOfMonth(date); i > 0; i--) {
                    Date d = new Date(date.getTime() + (i - date.getDay()) * 24 * 60 * 60 * 1000);
                    signMap.put(FormatDate(d, "yyyy-MM-dd"), ((v >> 1) << 1) != v);
                    v >>= 1;
                }
            }
        }
        return signMap;
    }
    
    private static int getDayOfMonth(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        int month = calendar.get(Calendar.MONTH) + 1;
        if (month == 2) {
            return 28;
        }
        if (List.of(1, 3, 5, 7, 8, 10, 12).contains(month)) {
            return 31;
        }
        return 30;
    }


}
  • 10
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值