从清档需求谈谈 Redis 二级索引的使用

从清档需求谈谈 Redis 二级索引的使用

原创 单汉强 网易游戏运维平台 2019-07-27

 

单汉强

网易资深运维工程师,游戏部 Redis 服务平台负责人。

本文主从业务提出的 FLUSHDB 需求引发的思考,实现通过引入额外的数据结构定位要删除的键。最后总结一个更通用的 Redis 使用姿势,即 Redis 中的二级索引的使用,并通过其他例子,说明如何用二级索引实现 Redis 的最大化利用。

背景

一个休闲的周末下午,我打开电脑,突然收到业务开发同学传来的一个信息:

「在吗?麻烦帮我开通一下 FLUSHDB 这个命令,我的程序需要用到。」

使用过我们 Redis 服务的同学应该知道,为了保证线上业务的稳定,我们把线上的 Redis 的一些危险操作都屏蔽掉,比如KEYS命令,其中也包括清理指定 DB 数据的命令FLUSHDB

这是因为 Redis 是一个单线程服务,命令逐个执行,一些耗时较长的命令可能会导致 Redis 卡住不能响应正常请求。不仅仅是卡住,在高可用环境下,我们用于监控存活状态的请求也无法响应,最终会导致被误判成故障,引发切换。

想了解 Redis 内部运作原理的同学可以参考这篇文章:

Redis 核心原理:基于事件的处理流程

「不行呢,这个会影响线上服务稳定性。」我如此回答业务同学。

「那有什么办法呢?」业务同学问。

几种常用的方法

SCAN

通常是最直接粗暴的代替方式,选择某个 DB 之后,通过SCAN命令扫描出所有 key,然后逐一或批量删除。

存在问题

  1. SCAN的方式使用逐步迭代的方式获取 key,对于线上还在读写的数据,可能会意外删除不想删除的数据。而业务就是要求不停机操作清档。

  2. 需要业务处理逻辑,每次扫描并删除的 key 的数量过多可能也会引起阻塞问题,过少则会影响效率,需要花很长时间才能清理完数据。

异步删除

Redis 4.0 之后,支持异步删除的功能FLUSHDB ASYNC,可以在不阻塞 Redis 主线程的情况下执行FLUSHDB功能。

存在问题

我们屏蔽命令的时候,没办法单独屏蔽FLUSHDB而开放FLUSHDB ASYNC功能,因此需要运维同学线上操作或者使用管理平台提供的清档功能。

而业务的需求是将这个命令集成在程序中经常执行,因此虽然理论可行,但实际操作有较大风险。

利用过期功能定期删除

基于个人经验,通常业务需要清档操作,一般是触发了某些条件,通常是到了某个时间点,或者某个事件发生。

如果跟时间有关系的过期删除,可以用 Redis 的 EXPIRE或者EXPIREAT命令,前者可以指定多长时间后被删除,后者可以指定在特定时间被删除,通常能解决大部分跟时间有关的过期删除需求。

存在问题

对于因事件触发的删除,基本上无能为力。

场景还原

看来不是简单几句就能解决的问题,于是让业务同学描述下具体的使用场景,结合我的理解,还原为如下的需求:

  1. 场景为一个线上商城服务,提供不同的商品供用户购买。商品有不同的属性,包括商品类型、所属游戏类型、价格、销量、上线时间等。

  2. 业务能够将商品按照指定需求进行排序,比如价格、销量、更新时间等,并将结果以顺序列表的形式,返回给前端显示给用户。

  3. 商品的数据存放在关系型数据库(例如 MySQL)中,每次新的查询会去数据库进行查询,并得到结果。因为查询的类型总是有限的,而且在一段时间内通常变化不大,所以业务将这个查询结果缓存在 Redis,以便提高查询的效率。

    以查询的条件组成 key 的名字,查询到的结果存放在一个 list 的数据结构中。

    假设一个按价格从低到高的查询:

业务直接拿到结果之后就避免了一次去数据库查询排序的操作,降低数据库的负载,同时提高页面相应的速度。

  1. 当某个商品的属性发生变更,比如价格或者销量,存放在 Redis 的缓存记录就不再有效,需要删掉。用户再下一次查询的时候,会重新生成一条缓存记录放在 Redis 中。

大概场景听到这里,感觉还是一个比较常见的缓存需求,但是为什么要用到FLUSHDB这么危险的命令呀?不是应该只要删除跟某个商品相关的缓存就可以了吗?

业务同学的想法还是比较直接,当某个商品的属性发生变化的时候,直接清档就行。那些本来仍然有效的数据被删掉也没有关系,反正直接去数据库重新生成就行。

这样做大概是不太科学的,如果数据量比较大,每次清档后就相当于有一大波新的请求打到关系型数据库上,如果并发比较高的话,关系型数据库撑不住。一段时间内缺少缓存加速,也会很影响用户的使用体验。

解决思路

业务同学同意这样的方式确实不是很科学,于是决定在业务逻辑去判断要删除哪些 key。

但实际操作并不容易,业务逻辑需要根据商品的属性和信息去组装相关 key 的名字,并通过扫描所有匹配的 key 来删除。因为每次查询的条件不同组成的 key 名字组合不一样,可能需要多次全表扫描才能够删除干净。

另一方面,如果商品缓存起来的查询结果,只记录前面 N 个记录,这 N 个记录里面不一定包含这个被更新的商品,那相当于做了不必要的删除工作。

难道 Redis 就只能这么笨重地解决这类问题吗?

想想问题的本质,其实就是需要根据商品找到与之对应的查询语句的 key,那能不能直接维护一个商品与查询之间的关系?

商品(product)和查询语句(query)之间是一个多对多的关系,在 Redis 里面数据是 key-value 的方式存储,value 可以是不同类型,包括 list, set, sorted set 这一类集合。只要以商品的某个信息作为 key 名,比如product:$id,把相关的查询的 key 名作为元素存到一个 set 集合里面,就能创建这种映射。

每当某个商品的属性发生变化,就可以遍历这个 set 里的元素,逐一或批量删除或者修改,就不用遍历整个 DB,更不用清理整个 DB 的数据了。

但是与此同时,每次创建查询缓存的操作,除了原来的创建 list 之外,还需要额外更新对应商品的 set。

LPUSH query:xxxxx:xxxxx
SADD product1 query:xxxxx:xxxxx
SADD product2 query:xxxxx:xxxxx
SADD product3 query:xxxxx:xxxxx
...
SADD productN query:xxxxx:xxxxx

上述操作可以打包成一个批命令,利用 Redis 的 pipeline 功能批量操作。

虽然这里更新的操作显得有点多,但在业务读多写少的场景下应该还是可以接受的。

这时业务同学点了个赞,就开始敲代码去了。

本质需求——Redis 的二级索引

总结这个应用的情景,可以提炼出一个的使用姿势 —— Redis 中的二级索引。

下面我用另一个场景说明。

选择特定年龄范围的用户

假设有三个用户,每个用户的信息用一个 hash 数据结构保存。

HMSET user:1 id 1 username antirez ctime 1444809424 age 38
HMSET user:2 id 2 username maria ctime 1444808132 age 42
HMSET user:3 id 3 username jballard ctime 1443246218 age 33

这个时候如果想要找出介于 30-39 岁之间的用户,在没有其他数据结构支撑下,只能通过HSCAN命令全 DB 扫描每个用户的 age 字段,才能找出来。

我们可以用sorted set来记录每个用户的年龄信息,score 是年龄,value 是用户 id:

ZADD user.age.index 38 1
ZADD user.age.index 42 2
ZADD user.age.index 33 3

这样,当我们要找出介于 30-39 岁之间的用户,只需要执行以下操作:

127.0.0.1:6379> ZRANGEBYSCORE user.age.index 30 39
1) "3"
2) "1"

这里 id 为 1 和 3 的用户,就是我们要找的用户的 id,然后再通过HGET命令查询具体的用户信息。

其实这是官方网站上关于二级索引使用的主题的一个例子。有兴趣的同学可以访问 https://redis.io/topics/indexes 了解。

二级索引可以说是 Redis 的一个相对进阶的使用方法,因为这种使用方式通常结合多种数据结构,比如 list, set 或者 sorted set 来使用。与此同时,二级索引赋予了 Redis 更加丰富的技术手段,来实现更复杂的查询和操作需求。

官方网站上还提到其他 Redis 在索引上的更高级使用方式,比如如何使用字典顺序索引( Lexicographical indexes)、组合索引、多维索引等案例,这里不作展开了。

回到本文的商城场景

在本文最初的线上商城的场景里面,我们有一个查询缓存到每个商品的映射关系(这里用了 list 结构)。但是反过来如果想从商品找到查询缓存,如果不借助额外的数据结构,就只能通过遍历的方式。

因此,我们通过以商品为 key 建立一个没有顺序要求的二级索引,用来获取相关的查询的 key 的名字,所以使用了 set 类型。而这里比较特殊的是,因为每个商品都需要一个这样的索引,所以我们使用多个 set。

最后,二级索引不是解决所有问题的银弹。实际上如果这个场景里面,数据量比较少(也不会增长),而且商品信息变更频率很低,说不定偶然来一次FLUSHDB要更痛快一点。一切还是要看具体业务需求。

往期精彩

 

IPv6 支持度报告和 IPv6 环境下 DNS 相关测试

网易游戏海外 AWS 动态伸缩实践

Swap 与 Swappiness

MongoDB Change streams 与数据订阅同步

MySQL Flashback 拯救手抖党

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值