基于Redis位图实现用户签到功能(golang)

场景需求

适用场景如签到送积分、签到领取奖励等,大致需求如下:

  • 签到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
	})
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值