背景:在被访问数据库中没有的数据时,此时会访问 db 。线上 90%的数据都是数据库没有的(业务场景限定)。此时为了解决缓存穿透的问题,引入了 redis 设置空值。此时导致的结果是(数据是 n x n )的。导致线上 redis 有 3 亿的空值 key 。
解决思考:如果使用布隆过滤器,受限制于不能删除元素限制。不能使用,有什么更好的解决方案吗?
1
qwer666df 2021-03-08 17:45:01 +08:00
设置了空值之后, 在加个过期时间?
|
2
shoushi 2021-03-08 17:48:31 +08:00
我对这个场景感兴趣
|
3
xx6412223 2021-03-08 17:51:28 +08:00
n*n 的 肯定有共同前缀吧,而且大部分都是空。按前缀设置一部分空值。
|
4
BinYang OP @qwer666df 过期时间已经加了,目前是设置了 3 天的过期事件(正常热点数据也是 3 天)。3 天 redis 的值大概会到达 2 亿左右。redis 内存消耗太快了。
|
5
BinYang OP @shoushi 场景就是上述的场景。
就是有一个表保存了点对点的关系。但是线上大多数点和点之间是没有关系的。所以 90%会打到数据库(如果 redis 不做缓存穿透的处理)。 为了做缓存穿透的处理,在数据库查到位空时,在 redis 放了一个 key 等于参数,value 为空的值。但是,这部分值太多了。 |
7
leopod1995 2021-03-08 17:58:31 +08:00 4
反向思维,整个 db 搬到 redis,9:1 的比例的话,大概 3000w 条数据就可以了吧?
查 redis 就是查 Db, 没有就返回 null 。 启动做数据从 db 到 redis 的冷同步。 唯一的问题就是 从 Db 同步更新 redis |
8
BinYang OP @leopod1995
这个方案,有考虑过。这么做的话,相当于强依赖 redis (这样子设计是不是有点蛋疼) 相当于 redis == db 了。一切以 redis 为准。db 相当于只是一个数据的存档了。 |
9
skymei 2021-03-08 18:02:50 +08:00
反过来呢,只缓存数据库有的数据到 redis,直接查询命中,没命中的数据库肯定也没有,就直接返空
|
10
Kinnice 2021-03-08 18:08:21 +08:00
有点意思,要不给 key 设置个 count,count 少于 xxx 的定时删除。反正也属于是非热点数据。
key value 关键词 A {"count":123,"value":""} |
11
helone 2021-03-08 18:08:38 +08:00
我局的 7 楼的方案就很不错啊,Redis 本身就是数据库,只不过因为高性能所以很多人当作缓存用罢了,强依赖 Redis 没什么不妥的,如果你担心缓存穿透问题,你也可以考虑加个自旋锁
|
12
jwenjian 2021-03-08 18:19:14 +08:00 via iPhone 4
bitmap ?
|
13
Numbcoder 2021-03-08 18:20:51 +08:00
1.优化 key 的长度,降低内存占用
2. 慢慢缩短过期时间,直到一个 db 能承受的穿透查询量 3. 调整 Redis 的内存淘汰策略 4. 加内存,硬件白菜价(相对人力成本),加个 8G 内存,没多少钱,轻松解决问题 |
14
Numbcoder 2021-03-08 18:25:31 +08:00
不太同意 7 楼的方案,理由:
1. 业务变得更复杂,数据一致性很容易出问题 2. 当你的业务数据增长之后,你会为此付出更大的迁移代价 |
15
gBurnX 2021-03-08 18:29:29 +08:00
建议楼主说一下具体业务,硬件配置等,说不定大家的建议又会不一样了。
|
16
bthulu 2021-03-08 18:43:22 +08:00
redis 设置 30 分钟过期, 前端 nginx 将同一个点的请求尽量路由到同一后台服务器当中, 业务代码再加一层本地内存缓存, 这样大部分请求本地缓存就搞定了, 到达 redis 的就没多少了
|
17
luckyrayyy 2021-03-08 18:47:44 +08:00
3 亿的数量级好像不大啊,想起来之前看微博的关注关系、点赞关系存储,直接存缓存就没问题,不过他们专门做了优化。还有另外一个场景,判断一个微博是否被某个人阅读过,这个关系更庞大,文章上说有上千亿的关系对,专门研发了个基于布隆过滤器的系统存储
|
18
lucienhsu 2021-03-08 18:51:14 +08:00
建议说明下具体业务场景
|
19
BinYang OP @Numbcoder 这个目前也是我们在做的。
已经做了,缩短过期事件,以及迁移到单独的 redis db. 同时已经在探索淘汰策略了。目前我们 redis 的大小是 128GB 。单独给这个业务使用。目前每天增长大概在 7GB 左右。目前来说,redis 满了之后。会出现部分打在 db 。 |
20
BinYang OP @gBurnX 目前配置是 128GB 的 redis,已经使用了 100GB 左右,key 的数量大概是 2 个多亿,不到 3 亿。
redis 失效时间,设置的是 3 天。(当前,预计在下一版本,降低失效时间到 2 天,逐步降低) |
21
BinYang OP @lucienhsu 场景,大概就是人和人的点对点关系。可以理解为微博的好友添加。
但是,大多数时候人和人不需要点对点关系。但因为一些业务上的需要,需要在接口返回中有一个内容就是是否和其他人存在点对点关系。此时会不停的去查库。因为不存在关系,redis 也命中不到。此时穿透到数据库。但是不能使用布隆过滤器(这个关系可能随时变化,布隆过滤器不支持随时的变更->多写的场景)。因此做的操作是,给 redis 放一个值为空的对象进来。来防止穿透到 db 。但是因为这个关系是 N X N 关系。比如,有 100W 用户,那就是 100W X 100W 这个数据量上限来说,是非常大的 |
22
Dganzh 2021-03-08 19:21:30 +08:00
过期时间太长了,24h 试试看
|
24
linvon 2021-03-08 19:37:44 +08:00
@BinYang #21 这种场景应该存储我的好友有谁(或者说我与谁是好友),而不应该存储我与谁是不是好友吧,怎么会扩散到 n^2 的数据量呢,用户总不能把其他所有用户都关注了吧
|
25
vpsong 2021-03-08 19:40:29 +08:00 1
布隆过滤器只增不删,命中了布隆过滤器的再去 db 查,db 不会连那 10%的量都撑不住吧
|
26
yzbythesea 2021-03-08 19:41:11 +08:00 2
我觉得问题核心是你这 90% 的数据都没有,但是你却还要请求你这个服务去查他们的值,感觉很奇怪。。。Redis 空值我觉得更多是一个 Hack 。
为什么不考虑先搞个索引服务,来过滤掉这 90% 的请求吗?比如加些业务逻辑判断下这个请求为什么就不可能有值。 |
27
palmers 2021-03-08 19:46:24 +08:00
我觉得这种应该把 redis 当做 db 来做的,redis + db 双写, 所以 db 只是作为一个备份使用 兜底策略, 然后本地内存作为缓存使用 就是这中间的淘汰策略和缓存时间需要好好调一下
|
28
MajorAdam 2021-03-08 19:49:19 +08:00
9 楼的方案可以啊,没有就是没有,不会主动去 db 查,保证写入的时候同步到 redis
|
29
dnhzm 2021-03-08 19:59:58 +08:00
像 24 楼说的,存储已有关系到 redis 当中,然后使用 lua 脚本处理你的请求,返回是否存在对应关系
|
30
BinYang OP @linvon 问题是缓存命中不到时,就会去 db 查。这个时候就大多数流量都会命中不到。所以去 db 查。就会导致大流量打入 db 。
|
31
BinYang OP @yzbythesea 业务逻辑上是判断不出来两者是否有关系的,业务需求最后就是要查询这个人和其他人是否有关系。所以要去关系表查(实际上少数人有关系),此时。其他没有关系的人,因为 redis 命中不到关系数据就会一直打到 db 。此时为了解决这个问题,引入了 db 查询不到的情况下,设置一个 value 为 null 的值到 redis 中。此时就会导致 redis 有很多为 null 的对象(但是可能这个对象是热点对象)。比如有的人,总是被查是否和其他人有关系。
|
32
BinYang OP 目前还是比较支持 13 楼的答案,当前来说。
7 楼的方案问题,13 楼也做了答复。 |
33
young1lin 2021-03-08 20:11:40 +08:00
128G 的单个 Redis 实例?秀啊。返回一个 “null” 给 Redis,设置过期时间。
|
34
rrfeng 2021-03-08 20:34:25 +08:00
这个设计就有问题……对于此种简单关系表,直接查数据库并不会慢。我们之前用 MongoDB 抗几十亿关注关系查询,毫无压力。
|
35
rrfeng 2021-03-08 20:37:13 +08:00
MySQL 估计也没问题,专库专用,预先分表,加大内存即可。
|
36
luozic 2021-03-08 20:44:26 +08:00
|
37
firefox12 2021-03-08 21:08:20 +08:00 via iPhone
为什么存 n*n 这么大呢? 难道不应该一个人一个 hash 吗? 没有就去数据库里拿。查完就存在 redis 里 这样 1000 万 也只有 1000 万 hash table
你这样设计微博 12 亿注册用户 它的 redis 怎么搞? |
38
xuboying 2021-03-08 22:19:18 +08:00 1
虽然没有这方面的具体业务经验,但也觉得 op 的设计应该先转化为一个论证过程。再落实到具体的代码。
比如数据库和缓存总是有个同步过程,为什么觉得 redis 命中空结果是 OK 的,也许此时数据库里已经有数据了?这个同步的过程是怎么实现的。为什么不能反过来,redis 的有数据就是数据库的有,没有就认为没有。。。 |
41
coreki 2021-03-08 22:54:31 +08:00 via Android
redis 没有 key 就是没关系。业务层面,如果用户之间添加了关系就主动把 key 加到 redis 。这样就节省了 redis 的 90%内存
|
42
Orlion 2021-03-08 23:04:20 +08:00 via Android
7 楼回复应该是个很常见的设计,我司 redis 就分为缓存(不需要持久化,所有 key 都有过期时间)与持久化数据两种实例,楼主可以做个参考。如果麻烦的话,可以考虑该场景能否使用 bitmap 来做过滤
|
43
nagatoism 2021-03-08 23:17:55 +08:00
@BinYang 表结构感觉设计有问题,不如你要查 A 和 B 有没有关系,你应该问 A 的那个表有没有 B,而不是 A-B 这条边是不是存在在图里。
既然是一个稀疏图,为什么要用邻接矩阵而不是 neighborhood vector 来存储查询,这不是给自己找事? |
44
murmur 2021-03-08 23:20:04 +08:00
如果有人恶意访问冷数据,应该考虑风控直接干掉他而不是在你的数据上做文章
而这个情况基本就是爬虫,照着列表一顿遍历不管时间新旧 |
45
Aidenboss 2021-03-08 23:49:51 +08:00
1. 继续使用 bloomfilter,如果发生关注行为,则写入到 bloomfilter ;如果发生取消关注行为,则不对 bloomfilter 做处理。这样,bloomfilter 可以过滤掉大部分不存在关系的数据(因为 bloomfilter 是必然不存在),需要定期重建。
3. redis uid -> bitmap,控制每个用户的 bitmap 长度控制在 [0, N)。如果 A 关注了 B,则 B 对应的 bitmap[ hash(A uid) % N ] = 1,相当于对 B 的关系链分桶。如果 bitmap[ i ] = 1,说明可能存在关注关系,再去 mysql 表回溯。 前面还可以将 {A_uid}_{B_uid} 作为 key 写入到 redis 中,防止回溯到 mysql 的请求打穿。 |
46
Aidenboss 2021-03-08 23:50:11 +08:00
1. 使用 uid -> redis set 的方式来判断关系。这个是最容易搞的。微博每人关注上限是 5k,一个用户也就 5k 个关注,存储上可以接受。
2. 继续使用 bloomfilter,如果发生关注行为,则写入到 bloomfilter ;如果发生取消关注行为,则不对 bloomfilter 做处理。这样,bloomfilter 可以过滤掉大部分不存在关系的数据(因为 bloomfilter 是必然不存在),需要定期重建。 3. redis uid -> bitmap,控制每个用户的 bitmap 长度控制在 [0, N)。如果 A 关注了 B,则 B 对应的 bitmap[ hash(A uid) % N ] = 1,相当于对 B 的关系链分桶。如果 bitmap[ i ] = 1,说明可能存在关注关系,再去 mysql 表回溯。 前面还可以将 {A_uid}_{B_uid} 作为 key 写入到 redis 中,防止回溯到 mysql 的请求打穿。 |
47
luozic 2021-03-08 23:52:52 +08:00
|
48
wangluofansi 2021-03-09 00:17:20 +08:00 via iPhone
思路一开始就错了,这种判存在的问题肯定是布隆过滤器或者位图。你说的不能删除元素问题,首先想想是否一定要到元素级别的删除?是否能接受批量级别的删除?如果可以,建立一千个布隆过滤器,每个负责千分之一的 uid (须选择足够随机的哈希方法),过期时间为一天加随机时间( 60 分钟内)。这是简单方案,仍可能有 0.1%级别的流量穿透,所以查询数据库限流也是必须的。更可靠的方案就复杂了,有必要再考虑。
|
49
yzbythesea 2021-03-09 06:37:46 +08:00
@BinYang 那这个就是类比一个好友功能,是吗?这本来就是挺难的一个问题。我的意思是你优化的方向错了,我觉得 redis 空值只要 QPS 能顶住,就水平扩容好了。但是从工程角度讲,如果你们有足够的时间,不应该用 redis 空值这个思路,而是去建立一个服务器专门返回这个用户以及和他有关系的人(类似于好友)。
|
50
sadfQED2 2021-03-09 08:24:51 +08:00 via Android
1.布隆过滤器不能删除,你可以搜一下布谷鸟过滤器,这个可以删除
2.所有数据落一份到 redis,redis 使用集群部署,几亿数据都不是什么大事 |
51
wuqingdzx 2021-03-09 08:27:31 +08:00 via iPhone
定期重建布隆过滤器就好了,搞那么复杂干嘛。
|
52
sampeng 2021-03-09 08:33:18 +08:00 via iPhone
明显是设计问题…
90%/数据库都没有的数据,你却要求缓存有…你品…你细品 |
53
whileFalse 2021-03-09 08:50:18 +08:00
3 亿 key 才能占多少内存啊,加内存就是了。
布隆过滤器有错误率,不能用。 |
54
whileFalse 2021-03-09 08:52:02 +08:00
@BinYang 以 Redis 为准,数据库做持久化有什么问题吗?甚至都可以不用数据库,直接 Redis 持久化。
|
55
shyrock 2021-03-09 09:29:30 +08:00
水平不够,说实话没看懂 lz 的需求。
数据库和 db 是不同的术语吗? 既然是访问数据库,为什么又 90%的线上数据在数据库中没有? |
61
BinYang OP @wangluofansi 确实,可能之前的思路上是有问题的。这个是历史问题,暂时来说,还不可能推翻重来。肯定是在当前的基础上看怎么能优化出来使用先。
|
63
fengpan567 2021-03-09 10:05:44 +08:00
布隆过滤器,value 为 null 的过期时间可以短一些。或者考虑上 elasticsearch 吗(
|
64
AxEqaq 2021-03-09 10:42:20 +08:00
搭个分布式 redis,redis 接入 mysql binlog 或 canal,让 redis 做全量 mysql 数据的同步
这是电商厂各种商品系统的标配策略 如果内存成本太高,可以换成 ssd |
65
pangleon 2021-03-09 10:43:58 +08:00
我说一个没人提的吧,社交关系这种可以用图数据库,NEO4J,NEBULA 等。
保持图数据库和数据库的同步即可。但这会引入额外的复杂度和成本。 考虑到你们目前的数据量并不大。。。没必要这么折腾。 但是如果是搞社交的图数据库会有更多应用场景在你们那。 |
66
wuqingdzx 2021-03-09 10:45:42 +08:00
@BinYang 老哥思维不要局限了.
可以有个 Buffer 的布隆过滤器 假如布隆过滤器重建的粒度是每小时一次. 用一个异步任务每个整点前重建一个带有下一个时间段标识的布隆过滤器. 业务上使用布隆过滤器的时候按当前所处的小时选择对应的布隆过滤器. 例如现在时间 10 点 37 分, 你用的是 bloomfilter_0 . 到了 10 点 55 分, 11 % 2 = 1, 异步任务重建一个 bloomfilter_1.你到了 11:00 以后进来的请求就用的是 bloomfilter_1 了. 到了 11 点 55 分, 12 % 2 = 0, 异步任务重建一个 bloomfilter_0.你到了 12:00 以后进来的请求就用的是 bloomfilter_0 了. 以此类推 |
67
iseki 2021-03-09 10:50:42 +08:00 via Android
布隆过滤器定时重建就可以吧,本来就是一个有假阳性的东西,删除元素什么的不存在的
|
68
BinYang OP @wuqingdzx 定时重建的话,还是会在某个时间段内,关系是保持久的关系。这种不实时的话。业务上是不能接受的吧。个人理解,勿喷。
|
69
wuqingdzx 2021-03-09 11:44:23 +08:00
@BinYang 不太明白对你的实时性影响是什么.
布隆过滤器在这里是减少你大量的空值 key. 首先你的每个有效的 key 写入了布隆过滤器.布隆过滤器存储的是你可能有效的值,反过来说布隆过滤器没命中,那么这个 key 或者说 这个关系一定不存在,就没有必要再查 mysql 了 你的关系还是在 redis 和 mysql 里. 你的关系直接查 redis 缓存.查不到才走布隆过滤器. 布隆过滤器不存在,那 db 一定是不存在的.(按你 9:1 的比例,这足以挡掉 8 成以上的无效请求了) 布隆过滤器存在,那你就查 db 是不是真的存在,真的存在就继续 redis 缓存一下,不存在继续写入 redis 空值 key. (注意这部分请求很少,因为你大量的有效 key 已经命中 redis 直接返回了.) |
70
tolza 2021-03-09 11:53:28 +08:00
订阅 mysql 的 binlog,做主动变更 redis 中的缓存。相当于全量缓存了 db
|
71
daijialong 2021-03-09 11:58:29 +08:00
感觉设计有问题
|
72
BigR 2021-03-09 12:27:09 +08:00
这种数据一定要放在服务器上查询么,可以把个人的已有的点对点信息放到每一个终端呀。每次操作同步到终端。验证的时候可以在终端验证。担心数据不一致,可在验证的时候,去服务器上请求当前的点对点信息。
|
73
15190049162 2021-03-09 13:28:40 +08:00
我想知道为什么会查出大量的不存在的好友关系...这是什么业务场景
|
74
freeminder 2021-03-09 13:42:36 +08:00
布隆过滤器存数据库有的,命中了才去 DB 二次验证也行吧。这种情况下的 BF 就是只加不减了
|
75
zhgg0 2021-03-09 14:00:10 +08:00
没看明白定时重建布隆过滤器为啥不能解决问题。就算删除比较频繁,布隆过滤器也能挡住绝大多数无效请求。
布隆过滤器里面存在就去查 redis 、db,如果最终发现不存在就缓存空值,缓存时间可以设为布隆过滤器重建时间。 这样 redis 中的空值极限情况最多只有一个重建时间里删除的数量。 |
76
azhi 2021-03-09 15:07:10 +08:00
觉得你设置空值 key 的逻辑,有点别扭,这不就是不论是否命中都只查 redis 了,既然这样,我觉得可以不设置空值 key,缓存无法命中的时候也不用查库了,证明没有好友关系
|
77
no1xsyzy 2021-03-09 15:09:07 +08:00
数据库
你是搞了个 (User, User) -> Bool 的查询吗? 不要这样,你应该写个 User -> User[] |
78
yc8332 2021-03-09 16:03:55 +08:00
90%没有,那为什么不把数据库里的设置到缓存。都不用去查数据库,而是数据库主动更新到缓存。
|
79
HanLi2021 2021-03-09 16:19:56 +08:00 1
我们业务有类似场景,推荐用布隆过滤器,支持实时
1 实时修改的数据(新增 /删除),使用 Redis 缓存(过期时间大于布隆过滤器重建一次的时间就好了,比如 1 小时 ) 2 全量数据使用布隆过滤器,对于 3 亿的数据,内存只需要大概十几 MB,可以定时重建(比如每 30 分钟一次)。 3 穿透布隆的再加上临时缓存,比如 GUAVA 等包装下 DB 查询 判断逻辑 if (Redis 中 key 存在) return Redis value if 布隆不存在 return false else return guavaCache.get(key) |
80
xiangbohua 2021-03-09 16:20:37 +08:00
来个数据预热?
我的理解里面 3 亿空值难道很多吗? 缓存很多时候是解决速度问题,如果压力不高就让他穿呗,如果压力很大,那就来个数据预热? 有一点不太明白,你来个空值的 key 的话能解决问题吗? 逻辑上会不会反会 null 而出现逻辑错误呢 |
81
xiangbohua 2021-03-09 16:32:55 +08:00
@HanLi2021 具体场景可以类比一下么?没遇到过这种场景
|
82
linxb 2021-03-09 16:40:50 +08:00
用 Hash 类型试一下?一个 Hash 可以存储 40 多亿键值对,内存占用比 key-value 小很多
|
83
pipi32167 2021-03-09 16:46:13 +08:00
直觉是个 X-Y problem
|
84
FaceBug 2021-03-09 17:01:19 +08:00
@Aidenboss
我一开始思路也是用方案 1,即用 set 来存储,我实验了下 set 、setex 、sAdd 、zAdd 几种方案 以 100 万条 UID 为例 string+过期时间:就是楼主现在用的,key 为 UID,value 为 null,设置过期时间,内存占用 122M string:内存占用 92M,但不能自动设置过期时间 sadd:根据 uid%10,分为 10 个 set (如果每个 UID 一个 set 占用会更大),占用 76M,缺点是无法知道 uid 是什么时候插入的 zadd:同 sadd,score 设为 timestamp 为 115M,优点是可以手工删除到期的 uid |
85
securityCoding 2021-03-09 17:12:00 +08:00
布隆过滤器明显可以用啊,它的误差率表示:如果返回不存在则一定不存在,返回存在则可能存在。
先用一个专门的 redis 集群挡住 90%的无效流量,剩下的应该就可以处理了。 |
86
dqzcwxb 2021-03-09 17:37:49 +08:00
没人考虑过 Caffeine 吗
|
87
guijianshi01 2021-03-09 17:59:07 +08:00
1. 缩短空缓存时间
2. 过滤器内没有清除已经删除数据影响不大,导致写入部分空幻存 3. 定时刷新过滤器 4. 加内存,除非性能要求很高,否则不推荐直接将 redis 当做库,全量拉到 redis 里,两个同步成本稍高,具体更具自己业务确定 |
88
FaceBug 2021-03-09 18:06:54 +08:00
另外问一下
A-B 和 B-A,是视为两条关系,还是一条关系 如果不存在单向关注,都是互关这种逻辑 其实可以只存 UIDA-UIDB 一种情况就行了 |
89
NUT 2021-03-09 18:24:20 +08:00
如果 qps 要求不高可以使用 360 的 pika 等 基于 rocksdb 的 redis 变种实现。
这些中间件都是经过验证的。 甚至 pika 可以前面加一个 codis 作为 proxy 另外数据肯定要分冷热。 冷数据还有一个逐渐变冷的过程。 其实题主就是来平衡成本和性能的。 大家抓核心。 楼主你说对吧。 |
93
xuletter2021 2021-03-09 23:24:28 +08:00
@HanLi2021 赞同 79 楼的做法,但逻辑顺序可以,先查询布隆过滤器,布隆过滤器根据业务周期性更新,结果是不存在直接返回,存在再去查 DB 或 redis,这样先拦截 90%的流量,后面的流量具体看 DB 是否能抗住,实在不行分库分表的做法也可以,这样整体方案简单;如果不能可以用 redis 缓存下大部分的流量,但需要同步 DB 中的数据到 redis,一种方式是双写,复杂度高,一种是只写 DB,使用自动同步工具同步 DB 中的数据到 redis,稍微简单。但这些都引入了额外的复杂性。其实还是倾向只用布隆过滤器和 DB 分库分表,实现方案简单。
|
95
no1xsyzy 2021-03-10 11:20:52 +08:00
|
96
rylei 2021-03-10 17:57:52 +08:00
感觉就是用户关注的业务,这样正常来说只需要存储用户的关注列表即可。100W 也就是 100W 个 hash
|
97
rylei 2021-03-10 18:01:25 +08:00
#37 楼方案是可以的
对于#57 查询两个人是不是好友,直接查询两次 redis, 或者用 lua 脚本一次将数据取回即可 |