10W+TPS高并发场景我的浏览记录系统设计方案

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

eb1e778b560b4632340b7fb871a4c75a.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

来源:juejin.cn/post/
7276678098409898019


背景

用户的浏览行为并发量极高,在下单前用户会浏览多个商家、多个商品,在内心反复纠结后下单。电商系统浏览记录的写入量往往在订单量的十倍甚至百倍,如此高的写流量并发对于系统的挑战可想而知。

最近团队里接了一个需求,要求扩展浏览记录的品类,除了支持门店的浏览记录,还要支持商品、视频等内容信息的浏览记录,未来接入更多类型的浏览记录。值得一提的是用户只能查看30天以内的浏览记录,30 天以外,无需归档保存。新改动的需求场景,预估写入流量比门店维度会高出十倍。高峰期的写入流量预估达到了100K TPS+

在 App 主页也需要透出浏览记录,支持用户分页查询。考虑到用户主动查询浏览记录频率不高,读取浏览记录的 QPS 在1K-10K级别。

从读写场景分析,浏览记录的写流量要高于读流量,但是高峰期读写都会超过10K QPS,所以存储中间件的选取非常重要。

c38393c04ec4cc3810de98d4f90a2824.png

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

存储模型选型

接下来我将从数据可靠性、数据成本、读写性能等方面 探讨存储系统的选型。

从存储容量上看,用户的浏览的店铺、商品假设每天访问 10 个(保守估计),存储 id和时间戳 大概占 20个字符,一共 200B。浏览记录支持 30 天,假设 1 亿(保守估计)活跃用户。那么全量的数据规模大致为 20 * 10 * 30 * 100000000/(1024**3) = 558 G。保守估计30 天的浏览记录在500G 以上。

指标\中间件RedisRedis + MySQLMySQLTair
成本中等中等
数据可靠性不可靠可靠可靠可靠
读性能极高极高
写性能极高
实现难度

首先应该排除两个方案, 纯Redis和纯 MySQL方案

  1. 纯 Redis 成本高昂,500G以上 的内存存储成本太高。最重要的是无法保证数据可靠性。存在丢失数据的风险。

  2. 纯 MySQL方案在高峰期 读写流量的性能无法保证。且每天需要归档数据,系统实现难度高。

其次来看 Redis+MySQL 方案也是不行。

  1. 写入时依然需要同时写入缓存和数据库,数据库抗 10W 写入流量,稳定性压力还是非常大的。对系统的挑战巨大

  2. 数据库存储了全量的浏览记录,需要每天归档数据。也就是系统流量除当天的写入流量,还要包括归档删除的流量。

综合分析下来,Tair 在成本、数据可靠性、读写性能、实现难度方面都比较优异。

Tair最初是诞生在淘宝,定位是作为数据库之前的缓存,最开始用在了淘宝的用户中心;我们花了5年时间,将Tair从缓存演进到分布式KV存储兼备,场景也全方位覆盖了互联网在线业务,例如从淘宝的用户登陆、购物浏览、下单交易、消息推送等等都会访问Tair。

Tair 可支持持久化的存储,同时实现了Redis 的原子化协议接口,我司基于 Tair 进行了改造。可保证100K的读写性能。后来的线上压测在2W QPS 查询时 TP999 能有 5ms的优异表现,实在让我刮目相看。更多的黑科技大家可自行百度 Tair 即可。

接下来大家可简单认为 Tair 是实现数据持久化,读写性能稍差的 Redis。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

存储结构设计

用户各种类型的浏览记录,要支持分页查询,有多种实现方案。为了读者便于理解,我先基于redis分析,最后再对比Tair和redis区别,选择Tair合适的方案。

List结构

List结构最简单,使用List存储用户的商品浏览记录,将最新浏览记录插入到List头部。

LPUSH KEY_NAME VALUE1
新增记录:LPUSH ${userId}  {$productId, $viewTime}

分页查询,需要指定起止offset, 截止offset= offset + count即可
LRANGE ${userId} ${offset} ${offset}+${count}

Redis List LPUSH新增时,会在队列头部新增,这样保证第一条浏览记录是最新的。使用LRANGE 可以指定范围查询浏览记录。

Tair的List结构和Redis类似,并且Tair支持 元素维度的超时。但是当前Tair只能在队列尾部新增记录,这样第一条记录是最早的浏览记录。分页查询最新浏览记录只能从后倒序查询。但是遗憾的是Tair的查询只能从头开始,正序查询。所以是无法支持从最新浏览记录分页查询的场景。

Sort Set结构

ZADD KEY_NAME SCORE1 VALUE1
新增浏览记录:  ZADD ${userId} ${viewTime} ${productId}

ZREVRANGE key start stop 
分页获取数据: ZREVRANGE ${userId} offset count WITHSCORES

sort set支持 设置分数和value。在浏览记录中,可以使用 时间戳作为Score,商品Id等作为Value。分页查询时使用 ZREVRANGE 倒序分页获取浏览记录

redis无法支持Sort Set元素维度的过期。30天以上数据的删除,需要归档任务。

由于Tair也没有 Sort Set数据结构,所以 Sort Set方案也被抛弃。唯一剩下的只剩下 Hash结构。

由于List和Sort Set无法支持,最后只剩下了Hash结构

Hash结构

Hash结构较为复杂,总共三个方案。难点在于无法优雅的实现分页查询最新浏览记录。

方案\keykeyhash fieldhash value
方案 1${userId}${商品 id}时间
方案 2${userId}${day}json 存储 productId和时间

${userId}day_infojson 存储 productId和时间
方案 3${userId}${day}json 存储 productId和时间

${userId}_dayinfo${day}当天的访问总数

方案 2 和方案 3 都需要存储用户每天的浏览记录总数,区别在于 方案 2 把每天浏览数量存储在 Hash field 中。方案 3 把每天浏览数量存储在redis 大 key,且每天的数据存在Hash Field,即小key中。

接下来将分析各个方案的优缺点。

方案写入分页查询过期
方案 1写入原子分页查询:需要一次性取出30 天的浏览记录支持hash field 过期
方案 2并发写入问题。同时每天的浏览次数+1时,需要更新整个大JSON,网络耗时比较高。首先取出用户每天的浏览数,根据当前页数,计算取出哪天的浏览数据。无需取出全量浏览数据支持hash field 过期
方案 3并发写入问题。同时更新每天浏览次数时,只需要更新当天的数据即可首先取出用户每天的浏览数,根据当前页数,计算取出哪天的浏览数据。无需取出全量浏览数据支持hash field 过期

由于方案 1 ,分页查询时需要取出全部的浏览数据在客户端进行分页,当用户的浏览数据非常大时,一定会出现慢查询。同时客户端进行分页在 高并发场景下,内存的消耗也是比较高,系统 GC 压力会非常大。综合下来,方案 1 并不合适。

为了进行分页,要想一个好办法。如果把每天的浏览次数记录下来,每次分页时查询每天浏览次数,排序,最后根据当前页数计算要取哪几天的数据。这样好处是不需要取全部的浏览记录,只需要取某几天的浏览记录。

方案3和方案2都是这样的思路,但是方案3比方案2 要更加优异。要知道每次浏览行为都要更新当天的浏览次数,方案2 把所有的浏览次数放到一个大JSON中,每次更新都要更新一个大JSON。而方案3 把每天浏览记录打散放到Hash的Field中。明显写入时,性能更高

但是方案3也有劣势,把每天的浏览次数打散放到Hash中,计算分页数据时,需要全部取出Hash的所有Key。而方案2只需要取Hash的一个Field。两者的差异真的很大吗?其实不然……方案2、3都需要取出30天的浏览次数。区别在于查询一个HashField和查询Key的区别。从查询数据量上并未差异。

由于只记录了30天的数据,且只记录每天的浏览次数,所以一个Field Value 数据量并不大。例如2023.09.09共21条浏览记录,存储上field:"20230909",value: 21, 一共10个字符。30天一共300个字符。查询300个字符,对于Redis压力并不大。20230909还可以压缩,可以使用上线时间为基准时间,计算距离这一天的差值,一般不超过4位数9999。这样又可以把10个字符降为5个字符,这样一个Hash结构共150个字符,数据也不大。

业务场景决定了写流量远大于读流量。所以分页查询时每次取150个字符获取每天的浏览次数,然后计算从哪几天中获取浏览记录列表。整体上查询的耗时可控。

当然也会存在问题,例如某一天用户的浏览行为非常多,浏览的商品和门店信息都存储下来,分页查询时,查到这一天时因为浏览门店非常多,所以就会对系统造成较大压力。这种极端情况下不能完全记录用户浏览行为。如何应对也需要和产品沟通协调,是否限制记录每天的浏览记录数量,例如限制阈值30个。避免极端情况导致慢查询,影响集群性能。

截止目前基于Hash结构的方案3 暂时满足我们的诉求,但是还有一个问题需要解决。并发写入问题。

因为当天的浏览记录都存储到了一个hash Field中。当一个用户频繁更新时,可能导致同时更新同一个key。如何解决呢?

如果是Redis,可以手写LUA脚本,实现Hash Field更新的CAS原子操作。即先查询当天浏览次数,然后更新当天的浏览次数+1,如果成功则记录浏览行为。如果浏览次数大于阈值,则丢弃这次浏览行为。

但是Tair既没有实现Hash Field的CAS操作,也不支持LUA脚本,我只能想其他办法。

Tair实现了Key Value维度的CAS更新,是否可以使用userId+day作为key,浏览次数作为value呢? 不可以,因为用户量级太高。上亿用户 * 30天。将近30亿-300亿的KEY, Tair系统压力也是比较大。

如何确保用户维度无并发呢?

分布式锁方案

使用userId维度分布式锁,写入量级在10W TPS。

不作控制

如果出现并发问题,容忍数据丢失问题。实际上用户的两次浏览行为基本上会在秒级别,系统基本不会出现并发访问。

浏览行为消息 通过UserId进行分片路由。

浏览行为是通过Kafka通知的,如果把userId的消息全部路由到Kafka的一个分片,同时保证有序消费,就可以保证单个用户的浏览行为都是串行处理的。

综合分析开发成本,我们认为暂时不进行并发控制,因为我的浏览行为数据并不是强一致性要求的数据,即使有丢失对于用户也没有损失。同时并发修改的概率还是比较低的。我们认为 并发写入的风险可控。

总结

我的浏览数据典型的高并发写入、低并发查询场景。我们要从数据可靠性、成本、系统实现难度、读写性能等多个方面全面评估。落实到存储结构时,需要考虑存储系统支持的特性是否满足。

综合分析下来,让我意识到我的浏览行为 是一个对存储系统挑战极大的场景。有一个稳定可靠性能强大、功能强大的存储系统真的可以简化 业务逻辑的实现。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

35b8264d5013523521eaf1e41f88f068.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

0ba1c7f344865025cf61bf3bb5239a67.png

f014863bd8cbb70ed00f78f30e3ed0f0.png593d1050c6eeb293dd7b5647d17b30f9.png10a86deef756bcdbb8e1d06f125263e7.pngee646301041c55f4d07b9f4e4aab91be.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值