文章详情

短信预约信息系统项目管理师 报名、考试、查分时间动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

玩转Redis-京东签到领京豆如何实现

2021-07-08 09:13

关注

玩转Redis-京东签到领京豆如何实现

《玩转Redis》系列文章主要讲述Redis的基础及中高级应用,文章基于Redis 5.0.4+。本文是《玩转Redis》系列第【8】篇,最新系列文章请前往公众号“zxiaofan”查看,或百度搜索“玩转Redis zxiaofan”即可。

本文关键字:玩转Redis、签到记录、签到日历、签到领京豆、用户签到表设计、位图Bitmaps;

大纲

1. 京东签到日历的产品逻辑

京东签到领京豆

2. 传统关系型数据库下的实现方案

2.1. MySQL表设计

2.1.1 表设计初级玩法(80%的人只会这么玩)

  新建一张“用户签到记录表(user_sign)”,核心字段如下:

字段英文名 字段中文名
keyid 数据表主键(AUTO_INCREMENT)
user_key 京东用户ID(全局唯一)
sign_date 签到日期(如20200618)
sign_count 连续签到天数(如2)
# 查询用户小东(user_key="20200618-xxxx-xxxx-xxxx-xxxxxxxxxxxx")的连续签到天数;
# 注意:sign_date BETWEEN "2020-06-17" AND "2020-06-18" 关联的时间点是0时0分0秒,
# 所以此处SQL的时间点必须带上时分秒;

SELECT
	sign_count 
FROM
	user_sign 
WHERE
	user_key = "20200618-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 
	AND sign_date BETWEEN "2020-06-17 00:00:00" 
	AND "2020-06-18 23:59:59" 
ORDER BY
	sign_date DESC 
	LIMIT 1;

  签到用户量较小时这么设计或许勉强能行,但京东这个体量的用户(估算3KW签到用户,一天一条数据,一个月就是9亿数据),即使数据表按照月份分表,同时按照“用户ID”进行hash分表(数据表示例为“user_sign_202006_0”),数据存储也是巨大的挑战。关键是投入产出比太低,这种方式还是say goodbye吧。

2.1.2 表设计进阶玩法(按位存储,高级程序员才会的玩法)

  初级玩法一条签到数据一条记录,占用了大量的存储空间,我们可以从这里优化一下。

# 用户签到记录表user_sign_{h};
# 按照用户ID hash分表,h是hash值;
CREATE TABLE `user_sign_h` (
  `keyid` char(42) NOT NULL DEFAULT "" COMMENT "主键(签到月份+用户ID)",
  `user_key` char(36) NOT NULL DEFAULT "" COMMENT "用户ID",
  `sign_month` char(6) NOT NULL DEFAULT "190001" COMMENT "签到月份",
  `sign_record` int unsigned NOT NULL DEFAULT "0" COMMENT "签到记录",
  `sign_count` int unsigned NOT NULL DEFAULT "0" COMMENT "连续签到天数",
  `last_sign_date` char(8) NOT NULL DEFAULT "" COMMENT "上次签到日期",
  PRIMARY KEY (`keyid`),
  KEY `index_user_id` (`user_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

  表设计或许你有以下疑问,先思考一下吧,解析见文末Tips?

2.2. 查询签到情况及签到的技术实现

  以下技术实现基于“表设计进阶玩法(按位存储)”;
  keyid:由签到月份+用户ID生成。如用户小东(user_key="19980618-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),其2020年6月份的签到记录keyid值是"20200619980618-xxxx-xxxx-xxxx-xxxxxxxxxxxx",前6位是年月YYYYMM,后面36位是用户ID;

2.2.1. 查询用户当月签到数据

# 查询用户当月签到数据 @zxiaofan
SELECT
	sign_record
FROM
	user_sign_h 
WHERE
	keyid = "xxxx";

2.2.2. 查询用户连续签到天数

# 查询用户连续签到天数(服务器时间不是当月第一天) @zxiaofan

SELECT
	sign_count
FROM user_sign_h 
WHERE keyid = "当月xxxx";
# 查询用户连续签到天数(服务器时间是当月第一天)@zxiaofan

SELECT
	sign_month,	sign_count 
FROM user_sign_h 
WHERE keyid in ("当月keyid","上月keyid");

2.2.3. 签到

# 本月第一天签到SQL @zxiaofan;
# 新增一条签到数据,连续签到天数需判断上月签到记录的最后一次签到日期;
# 若上月最后一次签到是月末,则连续签到天数在上月基础上加1;
# 若上月最后一次签到不是月末,则将连续签到天数直接置为1;

INSERT INTO user_sign_h ( `keyid`, `user_key`, `sign_month`, `sign_record`, `sign_count`, `last_sign_date` )
VALUES
	( "本月keyid", "用户id", "202006", 1 << 1, 业务方计算好的连续签到天数 , "20200601" );
# 本月非第一次签到SQL @zxiaofan
# 本月第x天签到,则 sign_record = sign_record | (1 << x);
# 1 << x 表示:1向左移动x位;
# 假设今日是 20200602,则昨天是 20200601;

UPDATE user_sign_h 
SET sign_record = sign_record | ( 1 << 2 ),
sign_count = ( CASE last_sign_date WHEN "20200602" THEN sign_count WHEN "20200601" THEN ( sign_count + 1 ) ELSE 1 END ),
last_sign_date = "20200602" 
WHERE
	keyid = "10" 
	AND sign_month = "202006";

  补签需要更新对应日期的签到记录,计算并更新连续签到天数;不是本文重点具体的技术逻辑就不赘述了。

2.3. MySQL签到解决方案的注意事项

2.3.1. 并发签到如何处理?

2.3.2. MySQL签到记录解决方案的想象空间

3. 基于Redis的Bitmaps实现签到日历

3.1. 为什么要使用Bitmaps

  上述基于MySQL的进阶解决方案,已能满足海量用户的签到业务。但我们想再节省点存储空间,再提升响应效率呢。
  Bitmaps 闪亮登场。

3.2. 什么是Bitmaps

  Bit arrays (or simply bitmaps,我们可以称之为 位图 ),Bitmaps并不是一种实际的数据类型(比如Strings、Lists、Sets、Hashes这类实际的数据类型),而是基于String数据类型的按位操作。Bitmaps支持的最大位数是2^32位。
  位图本质是数组,数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们可以称之为 索引 )。
  Bitmaps可以极大地节省存储空间,使用512M内存就可以存储多达42.9亿的字节信息(2^32 = 4,294,967,296);

  Bitmaps常见应用场景:
① 各种实时分析;
② 存储大量与ID关联的布尔值,且希望极致节省空间;

  比如你想统计哪个用户访问网站的天数最多;则可以在用户每天登录时将对应天数的 bit位 设置为1,使用BITCOUNT统计此用户对应的字符串中为1的位的数量,从而计算出其登录天数。
  通常我们避免在Redis中使用大key,建议将大key拆分成多个小key。常规建议是单key仅存储1M信息,则可通过bit-number/M计算出key的名字,通过bit-number MOD M(MOD表示取余)计算出第几个bit位。假设 bit-number/M = 2,bit-number MOD M = 666,则对此位的操作实际是操作key名字为“xxx:2”的key,位数是第666的位。

3.3. Bitmaps如何使用

  关于Bitmaps的使用其实在先前的文章中已经提及过了,可以查看玩转Redis系列文章之《玩转Redis-Redis基础数据结构及核心命令》,其中在“String位操作”这一节已经讲过。此处我们来复习一下:

【Bitmaps核心命令】:SETBIT、BITOP、GETBIT、BITCOUNT、BITFIELD、BITPOS;

3.3.1. 【Redis-Bitmaps位操作】命令简述

命令 功能 参数
SETBIT 指定偏移量bit位置设置值 key offset value【0=< offset< 2^32】
BITOP 对一个或多个key执行逻辑操作,并将结果保存到destkey operation destkey key [key ...]【AND, OR, XOR, NOT】
GETBIT 查询指定偏移位置的bit值 key offset
BITCOUNT 统计指定字节区间bit为1的数量 key [start end]【@LBN】
BITFIELD 操作多字节位域 key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP/SAT/FAIL]
BITPOS 查询指定字节区间第一个被设置成1的bit位的位置 key bit [start] [end]【@LBN】

3.3.2. Bitmaps位操作命令注意事项

3.3.3. 【Redis-String位操作】命令详细对比分析如下

Redis-String位操作

3.3.4. Bitmaps位操作命令示例

# 位图Bitmaps位操作命令示例 @zxiaofan
# SETBIT、GETBIT、BITCOUNT、BITPOS 命令示例

// SETBIT 命令示例
127.0.0.1:6379> setbit bitkey 2 1
(integer) 0
127.0.0.1:6379> setbit bitkey 22 1
(integer) 0

// GETBIT 命令示例
127.0.0.1:6379> getbit bitkey 0
(integer) 0
127.0.0.1:6379> getbit bitkey 2
(integer) 1

// BITCOUNT 命令示例
// BITCOUNT、BITPOS的参数start、end指的是字节偏移量;
127.0.0.1:6379> bitcount bitkey 3 22
(integer) 0
127.0.0.1:6379> bitcount bitkey 0 0
(integer) 1
127.0.0.1:6379> bitcount bitkey 2 2
(integer) 1
127.0.0.1:6379> bitcount bitkey 0 2
(integer) 2

// BITPOS 命令示例
127.0.0.1:6379> bitpos bitkey 1 0 0
(integer) 2
// BITPOS 返回的是相对于第0 bit位的偏移量
127.0.0.1:6379> bitpos bitkey 1 2 2
(integer) 22
127.0.0.1:6379> bitpos bitkey 1 20 22
(integer) -1
# 位图Bitmaps位操作命令示例 @zxiaofan
# bitop 命令示例

127.0.0.1:6379> setbit bkey1 0 1
(integer) 0
127.0.0.1:6379> setbit bkey1 1 1
(integer) 0
127.0.0.1:6379> setbit bkey1 5 1
(integer) 0
127.0.0.1:6379> setbit bkey2 0 1
(integer) 0
127.0.0.1:6379> setbit bkey2 3 1
(integer) 0
127.0.0.1:6379> setbit bkey3 1 1
(integer) 0

// bitop AND
127.0.0.1:6379> bitop AND dkey1 bkey1 bkey2 bkey3
(integer) 1
127.0.0.1:6379> getbit dkey1 0
(integer) 0
127.0.0.1:6379> getbit dkey1 1
(integer) 0
127.0.0.1:6379> get dkey1
"x00"
127.0.0.1:6379> bitop AND dkey1 bkey1 bkey2
(integer) 1
127.0.0.1:6379> getbit dkey1 0
(integer) 1
127.0.0.1:6379> getbit dkey1 3
(integer) 0

// bitop XOR
127.0.0.1:6379> bitop XOR dkey1 bkey1 bkey2
(integer) 1
127.0.0.1:6379> getbit dkey1 0
(integer) 0
127.0.0.1:6379> getbit dkey1 5

3.4. Bitmaps实战签到日历

3.4.1. 签到场景下的Bitmaps设计

  1M数据可以存储‭1,048,576位(1 * 1024 * 1024 = ‭1,048,576‬),可以存储2870年的数据(‭1,048,57 / 365.25 = 2870.84)。
  所以我们使用1个key即可完全储存一个用户的签到数据。redis的key设计为 sign:user_key,value存储签到记录的位数组。

3.4.2. Bitmaps实现用户签到

  首先明确第0位表示哪一天的数据(数据基点),比如签到产品是2000年1月1日上线的(数据基点就可以是2000年1月1日),那么第0位就表示2000年1月1日的签到记录。想要记录2000年1月3日的签到记录,则先计算此时间点和数据基点的差值(差值为2),则2000年1月3日的签到记录将存储在第2位。其他日期以此类推。

# 指定日期签到,时间复杂度O(1) @zxiaofan

127.0.0.1:6379> setbit sign:user_key 2 1 

3.4.3. Bitmaps查询签到情况

  通过get命令查询指定用户的所有签到记录,然后在内存中计数即可。指定时间段的签到情况或者连续签到天数均可计算。

# 查询指定key的所有签到数据,时间复杂度O(1) @zxiaofan

127.0.0.1:6379> get sign:user_key

// 查询指定日期是否签到
// 先计算次日期与数据基点的差值x
127.0.0.1:6379> get sign:user_key x

3.4.4. Bitmaps实现签到业务总结

  从上述来看,使用位图Bitmaps实现签到业务场景相对简单很大,不必考虑跨月等问题,而且占用的存储空间也极小。那么我们还有优化空间吗?
  目前是1个用户仅1条记录,如果产品设计上不会存在跨年数据的操作,是否可考虑将签到数据按年存储呢,历年数据在持久化后从Redis中清除从而节省Redis内存空间。当然不要为了节省而拆分,如果导致业务逻辑变复杂,就得不偿失了。

4. 总结

4.1. 业务分析

  技术上11位的确足够了,但技术都是为业务服务的。如果使用简单的数字,则竞争对手就可以知道你的真实用户数量、用户增量情况,这在商业上是肯定不允许的。

  业务逻辑还是业务方做,在保证数据准确性的前提下,数据库逻辑尽量简单。

   auto_increment自增的确简单省事,但keyid自行设计为月份+用户ID,直接根据keyid查询指定用户指定月份的签到数据,这样不香吗。

   只能说可以实现以上产品逻辑,京东领京豆的实际产品逻辑更加复杂,比如,京东签到领京豆有个页面可以看到“京豆领取明细”,包含精确到秒级别的领取时间,这点以上文章并未涉及,当然这也不是本文的重点。
   每个产品的背后都有着产品经理充分调研用户需求、业务需求,架构、技术、运营等人员的通力合作。
   心存敬畏。

  京东的签到日历仅展示了当前月份的数据,支付宝会员签到的最大连续签到天数是7天,CSDN的签到次数仅保留3个月。
  为何不展示一年的数据呢?在商言商,不展示的重要原因当然是商业价值不足,投入产出比不高。技术上可以实现,但技术需要为业务服务、为产品服务。

4.2. 技术分析

   由于String数据类型的最大长度是512M,所以String支持的位数是2^32位。512M表示字节长度,换算成位需要乘以8,即512 * 2^10 * 2^10 * 8=2^32;

  Strings的最大长度是512M,还能存更大的数据?当然不能,但是我们可以换种实现思路,文中其实已提及,我们回顾下:将大key拆分成多个小key。常规建议是单key仅存储1M信息,则可通过bit-number/M计算出key的名字,通过bit-number MOD M(MOD表示取余)计算出第几个bit位。假设 bit-number/M = 2,bit-number MOD M = 666,则对此位的操作实际是操作key名字为“xxx:2”的key,位数是第666的位。
  按照这种思路,存储的大小完全不受限啦。

玩转Redis系列文章:
《玩转Redis-老板带你深入理解分布式锁》

《玩转Redis-如何高效访问Redis中的海量数据》

《玩转Redis-高级程序员必知的Key命令》

《玩转Redis-研发也应该知道的Connection命令》

《玩转Redis-Redis高级数据结构及核心命令-ZSet》

《玩转Redis-Redis基础数据结构及核心命令》

《玩转Redis-Redis安装、后台启动、卸载》

祝君好运!
Life is all about choices!
将来的你一定会感激现在拼命的自己!
【CSDN】【GitHub】【OSCHINA】【掘金】【语雀】【微信公众号】


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-数据库
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯