内容可能会一直更新, 记录一些觉得比较重要的小心得。
文章目录
value设置选择
value尽可能的不要只存单一的唯一标识符字符
注意这里的value
不是特指string
类型的value
, 而是所有数据类型的value, 在很多关联的场景下我们很习惯性的就把value设置为唯一标识符, 如id
, 后续用到的时候直接取出来就行。
但存在的问题是什么呢,假如后续需要根据这个存储的数据做业务扩展,就发现问题了,一个字符的id太过于单一, 我们应该选择存储为json
对象,这是个好习惯,当后续需要扩展其它属性的时候不会造成兼容问题。
举个例子,我们现在需要维护直播间的观众列表, 比如设置了一个hash
结构,key
的设计为hash:live:audience
, 当id为1的主播进入了一个id为2的用户,我们执行命令hset hash:live:audience 1 2
,因为我们的业务知识要维护观众列表,到时候能取出来观众列表就行了,所以这是满足要求的,可后续比如我还要统计观众在线时长的话,就一脸懵逼了,明明数据已经维护过了,但就是用不了,直接改原来的地方会造成线上兼容问题,只能再另存一份数据结构或者是强制兼容解析两种格式的value,但如果最初我们存的就是对象的json
的话,如hset set:live:audience 1 '{"id":2}'
则没有这个问题,后续有额外属性直接附加即可。
value有时候也需要追加唯一标识符或者关联关系唯一标识符
这种情况指的是什么呢,先举个简单的例子,我现在有一个道具库,道具库有自己的一堆属性, 然后每个用户可以获得道具,但是只能佩戴一个,现在设计一个hash
来存储用户正在佩戴的道具,而且希望能够拿到直接使用,一般一股脑的做法就是hash key
为用户id, value
直接把道具库属性拿来用就行了。如下命令,key 设计为hash:user_props:fit
, 打比方我们设置为如下数据
hset hash:user_props:fit 1 '{"propsId": 1, "propsName": "红色头饰", "propsColor":"red"}'
hset hash:user_props:fit 2 '{"propsId": 1, "propsName": "红色头饰", "propsColor":"red"}'
hset hash:user_props:fit 3 '{"propsId": 2, "propsName": "蓝色头饰", "propsColor":"blue"}'
如上, 即用户1和2佩戴的是道具id为1的红色头饰, 用户3佩戴的是道具id为2的蓝色头饰。
我们每次获取这个用户正在佩戴的道具直接hget
即可,没有任何问题。
但假如后面有个场景,这批人进到了一个统一的场景里,我们需要初始化这批用户的形象,在这个例子里其实就是我们想要批量获取到这批用户正在佩戴的道具,那么问题来了, 因为无论我们是使用hmget
还是pipeline
,这个时候命令返回的都是value
的集合, 而value
我们存的是原始数据,是没有关联关系的,最初我们的关联关系是通过hash key
和value
做映射的, 现在只有value
集合,发现我们丢失了hash key
来做映射,返回的集合我们已经没有办法去映射这些佩戴的道具是属于哪一个用户的了。
所以综合上面这种场景我们发现, value
的另一个好习惯是要把hash key
或者是关联的唯一标识符也存进去,即使只有value
这几个属性,也能包含我们所有需要的映射关系。
value附加属性比我们想象中的更加强大
上面的情况都是在说尽可能的让value属性更加丰富, 不仅是扩展问题还有后面的批量映射问题。但其实丰富value属性的作用其实是更加强大的。
比如一些抽奖类的业务,抽奖是有库存的概念的这种情况,一种是有限的库存,就这么多,中完了就没了,我们很容易的就可以设置数据结构为List
, 然后将奖品初始化push
到集合中,抽奖直接pop
即可。不选用Set
的原因是SADD
指令的复杂度是O(n)
, 而List
的PUSH
可以做到O(1)
。补充一下,上面的PUSH
和POP
只是队列中的概念,在实际Redis
数据结构中有不同的选择,因为选择不同,命令不同,故上面没有写实际命令。
但是还有一种情况是奖池是循环的, 在抽完之后按照最初的配置再刷一次,那么就有两种方式来处理这个过程, 一个是抽完之后再去刷,但是需要考虑并发问题,第二个就是时效性问题,再同步锁刷的时候,未初始化完成之前,其他用户抽奖都会被卡住,无论是控制并发还是时效性都不是很好的方案。
但是这个时候我们可以采用丰富value
属性的做法提前就做这个事情,由于奖品是我们预埋到集合中的,就拿List
来举例, 为了保证有序符合我们的规则,我们现在替换成具体指令, 添加奖品到奖池中使用LPUSH
, 而抽奖时采用RPOP
,先进先出的顺序来保证我们整个奖池符合我们后面的程序逻辑。由于在初始化奖池时,整个奖池的配置我们是清楚的,有多少奖品也是明了的。我们可以在奖池的value
中预埋一个属性,是否需要重新刷新奖池, 当我们抽奖代码RPOP
弹出来的奖品判断这个属性是最初我们设置的需要刷新奖池为true的时候,就去异步重新初始化奖池, 但是一定要采用抽奖顺序相反的顺序,即目前我们采用的RPOP
,那就要LPUSH
。 只有这样,虽然目前奖池中的数量大于配置中的一批次的奖品数量,才不会有问题,因为多出来的那部分一定是上一个阶段的抽完才会被抽到。那么现在就要去考虑有哪些奖品去需要将这个属性设置为true了,这个看自己业务实际情况决定,比如这个奖品是第二分之一的那个奖品时,或者是一些其他的控制,这个看自己。注意这个二分之一,这个奖品的计数,分母永远都是最初的同一个批次的奖品的总数(即奖池配置上的奖品总数),而不是剩余的奖品总数,而分子也永远都是这一个批次下的奖品总数从0开始的当前的循环角标,所以这个控制是非常简单的,分子分母都与剩余奖品或者已经抽了多少等等变数之类的值无关。
最后还存在一个问题, 如果仅仅依赖异步刷新,极限情况下也可能会出问题,如果异步刷新出了问题,奖池就可能抽空,没有保底就出问题了。所以采取办法,第一步是先尽量避免这个问题可以在业务允许的情况下,按照规则分隔,多设置两个可以刷新奖池的奖品。第二步就是最初的那个方法,同步全局锁重新初始化奖池,这个是作为保底必须存在的。万一真的出现了奖品异步刷失败的情况,也有托底的存在。
路由选择
集群版本使用的话,如果有一些业务, 需要强制将key路由到一个slot上,可以强制给一个统一的前缀,然后包裹住前缀。如
- {user}:base_info
- {user}:ext_info
这两个key会根据user
最终定位到相同的slot, 但是也要注意一个问题, 阿里云集群版本的redis
虽然内存很大,但是要看集群规则,比如2 * 8节点的,那其实一个节点只有两个G, 如果发生了key倾斜的问题, 即使总内存占用很小,但某个节点占用过高,也是会发生OOM的
HASH
用户维度与具体指标维度的选择
比如有这样的存储要求, 要存每个人的一些数据指标。如每个人的剩余奖池、新手抽奖次数、大奖抽奖次数等。
这个时候有两种维度,
- 每个用户一个key,
hash key
为每个指标
这样的好处是可以很方便的看一个用户的所有指标,在以用户为单位的情况下,指标越多可以越方便的查看一个人所有相关数据,并得出一些想要的数据结果。
而且如果用户很多,每个用户一个key,而不是一个指标下面很多用户为hash key, 这样一定程度伤也能避免大key的问题。
但是会造成两个问题, 一个是key会过多, 会多很多小key, hash key
的数量不多,其实小key过多应该也不算问题。第二个问题,就是站在更高的维度上去看数据,比如想要看全平台所有用户剩余的个人池大概是多少,由于key的分散,就非常难看到这些数据。
- 每个指标一个key,
hash key
为用户id
这样的好处正好完美解决方案1的缺点,想要看全平台某个指标的数据直接查询对应指标的value汇总即可。但是缺点是想要以用户的维度去看数据的时候,就需要多个指令才能看到了。但是指标的key一定远远低于用户作为key, 所以只是稍微麻烦一点,最终想要看数据相对来说也不是特别麻烦。
用户特别多的时候,存在大key风险。
总结
总结不出来,看个人选择吧。
key动态参数的占位前后问题
比如现在有个功能,有个id, 这个id会绑定很多指标,每个指标下还会有hash key, 那么存在两种设计方式
如
game:{transport_tycoon}🆔state
game:{transport_tycoon}🆔pre_reward_amount
这样的话如果找到id的话,这个id下所有相关的key都能很方便的找到,但是会存在的一个问题就是会导致由于id的数量太多,到id这一层级的key会特别多,不利于整体筛选
优点如下红框表示,找到id就可以看到下面所有相关的key
缺点如下图,同层级太多
总结:
还是总结不出来,看个人选择吧。