场景需求
适用场景如签到送积分、签到领取奖励等,大致需求如下:
- 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等。
- 如果连续签到中断,则重置计数,每月初重置计数。
- 当月签到满3天领取奖励1,满5天领取奖励2,满7天领取奖励3……等等。
- 显示用户某个月的签到次数和首次签到时间。
- 在日历控件上展示用户每月签到情况,可以切换年月显示……等等。
设计思路
对于用户签到数据,如果每条数据都用K/V的方式存储,当用户量大的时候内存开销是非常大的。而位图(BitMap)是由一组bit位组成的,每个bit位对应0和1两个状态,虽然内部还是采用String类型存储,但Redis提供了一些指令用于直接操作位图,可以把它看作是一个bit数组,数组的下标就是偏移量。它的优点是内存开销小、效率高且操作简单,很适合用于签到这类场景。
Redis提供了以下几个指令用于操作位图:
考虑到每月初需要重置连续签到次数,最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。Key的格式为u:sign:uid:yyyyMM
,Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,1表示已签,0表示未签。
例如u:sign:2:202105
表示ID=2的用户在2021年5月的签到记录。
# 用户5月17号签到
SETBIT u:sign:2:202105 16 1 # 偏移量是从0开始,所以要把17减1
# 检查5月17号是否签到
GETBIT u:sign:2:202105 16 # 偏移量是从0开始,所以要把17减1
# 统计5月份的签到次数
BITCOUNT u:sign:2:202105
# 获取5月份前31天的签到数据。u31:0-31的无符号数
BITFIELD u:sign:2:202105 get u31 0
# 获取5月份首次签到的日期
BITPOS u:sign:2:202105 1 # 返回的首次签到的偏移量,加上1即为当月的某一天
实现代码
redis包:github.com/go-redis/redis
实现代码:实现功能为主
package logic
import (
"fmt"
"github.com/go-redis/redis"
"time"
)
type UserSign struct {
}
//用户签到
func (s UserSign) DoSign(uid int) error {
//计算offect
var offset int = 2 //time.Now().Local().Day() - 1
var keys string = s.buildSignKey(uid)
resid:= GetRedis()
_,err := resid.SetBit(keys,int64(offset),1).Result()
if err != nil {
return err
}
defer resid.Close()
return nil
}
//判断用户是都已经签到了
func (s UserSign) CheckSign(uid int)(int64,error) {
var keys string = s.buildSignKey(uid)
var offset int = time.Now().Local().Day() - 1
redisclinet := GetRedis()
defer redisclinet.Close()
return redisclinet.GetBit(keys,int64(offset)).Result()
}
//获取用户签到的次数
func (s UserSign) GetSignCount(uid int)(int64,error) {
var keys string = s.buildSignKey(uid)
redisclinet := GetRedis()
defer redisclinet.Close()
count:=redis.BitCount{Start: 0,End: 31}
return redisclinet.BitCount(keys,&count).Result()
}
//获取用户首次签到的日期
func (s UserSign) GetFirstSignDate(uid int) (string,error) {
var keys string = s.buildSignKey(uid)
redisclinet := GetRedis()
defer redisclinet.Close()
pos,err := redisclinet.BitPos(keys,1).Result() //获取第一位为1 的位置
if err != nil {
return "",err
}
pos = pos + 1
var day int = time.Now().Local().Day()
var offsetDay int = (day - int(pos)) * -1
return time.Now().AddDate(0,0,offsetDay).Format("2006-01-02"),nil
}
//获取当月签到情况
//根据需要自己实现返回
func (s UserSign) GetSignInfo(uid int)(interface{},error) {
var keys string = s.buildSignKey(uid)
redisclinet := GetRedis()
defer redisclinet.Close()
var day int = time.Now().Local().Day()
var dddd string = fmt.Sprintf("u%d",day)
st,_:=redisclinet.Do("BITFIELD",keys,"GET",dddd,0).Result()
f := st.([]interface {})
var res []bool = make([]bool,0)
var days []string = make([]string,0)
var v int64 = f[0].(int64)
fmt.Println(v)
for i:=day;i>0;i-- {
var pos int = (day-i)*-1
var keys = time.Now().Local().AddDate(0,0,pos).Format("2006-01-02")
days = append(days,keys)
var value = v >> 1 << 1 != v
res = append(res,value)
v >>= 1
}
fmt.Println(res)
fmt.Println(days)
return nil,nil
}
func (s UserSign) buildSignKey(uid int) string {
var nowDate string = s.formatDate()
return fmt.Sprintf("u:sign:%d:%s",uid,nowDate)
}
//获取当前的日期
func (s UserSign) formatDate() string {
return time.Now().Format("2006-01")
}
//
package logic
import (
"github.com/go-redis/redis"
)
func GetRedis() *redis.Client{
return redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379", // use default Addr
Password: "", // no password set
DB: 0, // use default DB
})
}