目录
  • 前言
  • 基础知识
    • 位图源头
    • Mysql占用对比
    • 使用
  • 功能实现
    • 需求
    • 流程图
    • 签到周期获取
    • 签到存储key
    • 签到
  • 结语

    前言

    不会吧不会吧, 都2202年了还有人不会写签到? redis位图实现签到功能简单方便, 走过路过可不要错过呦!

    基础知识

    位图源头

    在日常开发中, 我们会遇到需要存储大量 bool类型数据的需求, 比如用户签到和用户登陆的记录等, 这个时候用mysql存储来说比较占用资源, 所以为了解决这个问题, redis提供了位图数据结构(就是 位数组), 每个 bool值只占用1个位, 8个位组成一个字节, 这样存储空间的节约率不用我多说吧;

    Mysql占用对比

    mysql 存一个thinyint需要占用1个字节(bool类型默认为thinyint(1)), 而且你还需要存一个主键Id(你不存也会自动隐性的帮你存一列), int的话需要占用 4个字节, 不算其他光是这两个字段存储你就需要5个字节, 而在位图里面5个字节都够存40条记录了…

    使用

    需要先知道以下几点:

    • 位图的内容实际上也就是字符串, 只不过是更改的个位的内容, 所以分为零存零取和整存零取;
    • 位图的位数是会自动补位的, 比如你设置一个空键第8位为1, 则会自动补充前8位为0 (数组从0开始计数);
    127.0.0.1:6379> setbit zero 0 1     // 设置第0位为true
    (integer) 0
    127.0.0.1:6379> getbit zero 0
    (integer) 1
    127.0.0.1:6379> setbit zero 8 1     // 设置第8位为true
    (integer) 0
    127.0.0.1:6379> getbit zero 8
    (integer) 1
    127.0.0.1:6379> getbit zero 7       // 前面会自动补位
    (integer) 0
    // 通过python方法获取 h 的二进制
    >>> bin(ord("h"))
    '0b1101000'
    127.0.0.1:6379> set one h         // 直接存入字符 h = 01101000
    OK
    127.0.0.1:6379> getbit one 0
    (integer) 0
    127.0.0.1:6379> getbit one 1
    (integer) 1
    127.0.0.1:6379> getbit one 2
    (integer) 1
    127.0.0.1:6379> getbit one 3
    (integer) 0
    127.0.0.1:6379> getbit one 4
    (integer) 1
    127.0.0.1:6379> getbit one 5
    (integer) 0

    功能实现

    看完上面的基础知识大家就都基本知道怎么实现了, 下面是实战环节

    需求

    • 以自然周为周期进行签到;
    • 展示签到周期;
    • 重复签到提示报错;

    流程图

    PHP利用redis位图实现简单的签到功能

    签到周期获取

    获取每个自然周的起始和结束时间

    // SignStartEndTime 签到起始时间&结束时间.
    func (slf *TaskService) SignStartEndTime() (startTime, endTime int64) {
    	now := time.Now()
    	weekDay := int(now.Weekday()) 
      // 如果是周日的话weekDay=0
    	if weekDay == 0 {  
    		weekDay = 7
    	}
    	startTime = time.Date(now.Year(), now.Month(), now.Day()-weekDay+1, 0, 0, 0, 0, now.Location()).Unix()
    	endTime = startTime + 86400*7 - 1
    	return
    }

    签到存储key

    因为是以自然周为单位, 所以设定key的格式为 user:sign:userId:20221107(周一的日期)

    // SignKey 签到key.
    func (slf *TaskService) SignKey(userId string) string {
    	st, _ := slf.SignStartEndTime()
    	return fmt.Sprintf("user:sign:%s:%s", userId, time.Unix(st, 0).Format("20060102"))
    }

    签到

    const (
    	SignTypeNo  = iota // 未签到
    	SignTypeYes        // 已签到
    )
    // Sign 签到.
    func (slf *TaskService) Sign(userId string) (code errcode.ErrCode, err error) {
    	var (
    		client  = redis.GetClient()
    		key     = slf.SignKey(userId)
    		weekDay = int64(time.Now().Weekday())
    	)
    	if weekDay == 0 {
    		weekDay = 7
    	}
    	val, err := client.SetBit(key, weekDay, SignTypeYes).Result()
    	if err != nil {
    		return errcode.ServerErr, fmt.Errorf("sign setBit err: %v", err)
    	}
    	if val == SignTypeYes {
    		return 已签到错误码, fmt.Errorf("sign already, val: %d", val)
    	}
      // 如果是第一次签到需要加个过期时间
    	signDay := client.BitCount(key, nil).Val()
    	if signDay == 1 {
    		client.Expire(key, time.Hour*24*7)
    	}
    	if err = 签到成功获取的奖励; err != nil {
    		// 如果添加奖励失败, 重置签到状态
    		if err = client.SetBit(key, weekDay, SignTypeNo).Err(); err != nil {
    			log.Errorf("sign reset err: %v", err)
    		}
    		return errcode.ServerErr, fmt.Errorf("sign add reward err: %v", err)
    	}
    	return errcode.Code200, nil
    }

    结语

    签到任务看着简单, 但实际上确实也不难, 但是我们在实现的时候要考虑到如何有效的节约和利用资源, 哪种实现方式会更好更优雅一些;

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。