redis 用武之地
缓存
这是 redis 使用的最多的场景,能极大提升应用程序的性能。当单个 MySQL 读写压力比较大,场景是读多写少的时候,把热点数据存储在更快的存储中,也就是 Redis。
读取数据
- 先从缓存中读取数据是否命中。
- 缓存未命中,则查询数据库获取数据,并把数据写到 Redis 中,让后续读取相同数据的请求命中缓存,最后把数据返回给调用者。
- 缓存命中,直接返回。
写数据
至于修改数据,程序员想了很多方法去尽可能保证 Redis 与 MySQL 的数据一致性。
- 先写 MySQL 数据,再删除 Redis 缓存数据。
- 监听 MySQL binlog 日志,修改 Redis 数据。
排行榜
使用 MySQL 等关系型数据库,非常麻烦,性能也差,而直接使用 Redis SortedSet 轻松搞定。
消息队列
简单消息队列,在一些不需要高可靠,但是数据量大会给 MySQL 带来非常大压力的场景,比如:到货通知、未读消息、邮件发送之列的。程序员可以使用 Lists 来实现一个队列。
分布式锁
Redisson 这个框架,就是使用 Redis 弄出了一套分布式锁解决方案。
计数器
Redis 的命令都是原子性的,程序员可以轻松地利用 INCR,DECR命令来构建计数器系统。
简介
快速上手:https://www.zybuluo.com/coldxiangyu/note/784495
注意名字空间的划分,通过在key上使用分隔符建立名字空间,redis in action的作者推荐冒号,但是头条看起来很多人喜欢使用减号
一、字符串
可以当作位图用
支持部分赋值和部分取值
二、列表
常用命令:LPUSH、RPUSH、LPOP、RPOP、LINDEX、LRANGE、LTRIM
注意redis中的索引从0开始,但是左右都是闭区间
rpoplpush用于把一个list中的元素弹出并放入另一个list
带b前缀可以将命令变成阻塞式命令,最常见的用例就是消息传递和任务队列
三、集合
常用命令:
集合运算:
表中没有列出的是DIFF,返回存在于第一个集合但不存在于其他集合的元素
四、散列
redis自身就是一个散列,因此可以吧redis的散列数据结构看作是一个小redis。主要功能是将逻辑上有关的数据聚合到一起,也可以看作是关系型数据库中的“行”。
像 HMGET 和 HMSET 这种批量处理多个键的命令既可以给用户带来方便,又可以通过减少命令的调用次数以及客户端与 Redis 之间的通信往返次数来提升 Redis 的性能。
其他命令
五、有序集合
常用命令
六、事务
Redis 有 5 个命令可以让用户在不被打断( interruption)的情况下对多个键执行操作,它们分别是 WATCH、 MULTI、 EXEC、 UNWATCH 和 DISCARD。
Redis 的基本事务( basic transaction)需要用到 MULTI 命令和 EXEC 命令,这种事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令。在 Redis 里面,被 MULTI 命令和 EXEC 命令包围的所有命令会一个接一个地执行,直到所有命令都执行完毕为止。当一个事务执行完毕之后, Redis 才会处理其他客户端的命令。
Redis 事务在 Python 客户端上面是由流水线( pipeline)实现的:对连接对象调用piepline()方法将创建一个事务,在一切正常的情况下,客户端会自动地使用 MULTI 和 EXEC包裹起用户输入的多个命令。此外,为了减少 Redis 与客户端之间的通信往返次数,提升执行多个命令时的性能, Python 的 Redis 客户端会存储起事务包含的多个命令,然后在事务执行时一次性地将所有命令都发送给Redis。
七、pipeline
一次请求打包执行多个命令。一般来说,建议业务 Mget/Pipeline 大小不要超过 20。对于 Pipeline 请求失败,请业务同学仔细 Review 并优化 Pipeline 大小。
- Pipeline对象调用Exec()之前,待发送命令均在内存buffer中Pipeline
- 对象调用Exec()之后,Exec()内部执行完之前,已经接收到的结果均在内存buffer中
实际使用经验(参考https://drive.google.com/file/d/1Loe6vS-1GC2LnQcLxyD87c3DLC5FhFqX/view)
1. 大 key
避免热key,比如,把大量热查询存放在一个key对应的一个map中,导致所有的查询都去一个机器请求类别信息,可选解决方式是使用了key前缀拆分大 key。
由于 Redis 是单线程、单租户的,当一个用户或其他上游服务访问一个大 key 时会造成这个大key所在的 Server 实例卡顿,进而导致其他访问该 Server 实例的请求超时报错,在业务侧的表现就是会有抖动报错。卡顿时间长短和 key 的大小及对这个大 key 访问的操作有关,如果持续访问大 key,会导致整个 Redis 吞吐严重下滑,超时报错持续增多,同时由于会在主从同步、故障恢复、数据迁移、扩缩容过程中传输整个大key,也会引起抖动,因此大 key 对 Redis 的稳定性影响较大,用户在设计KV 的时候请注意尽量不要有大 key。
消除大key最佳实践
检测
优化问题的必要条件就是发现问题。这里介绍在线离线两种方式,其中在线扫描的原理是:主线程会启动定时任务,周期性地扫描全表数据。为了避免影响线上服务,每次扫描不超过100个key,所以,此类渐进式的全表扫描会持续很长时间。 具体过程如下:
- 扫描 Key。
- 对于kv类数据:直接查看value大小,如果超过10KB,则记录。
- 对于非kv类数据:
- 首先查看field/subkey的数量,如果超过5000个,则记录。
- 如果不满足上一步中描述的情况,则估算field_value * field_number的大小,如果超过10MB,则记录。
- Configserver 会从Redis Server中获取大key数据,并校验大 Key 是否消除,再返回大 Key 列表。
- Configserver 根据Redis Server返回的数据进行metrics打点与告警。
另外一种方式就是离线导出 redis 落盘文件再批式处理,这里不赘述了。
迁移服务
用户可通过dorado的 Redis-Hive 将Redis数据全部导出,再去看有哪些大key,若这些大key是属于自己或者其他服务的,可与对应服务的负责人沟通,考虑将该服务迁移出去,新建一个Redis集群使用,避免互相影响。
拆分大key
结合具体业务场景,将大key进一步拆分为小key,例如一个string大key拆分成多个string重新写入,但拆分后数据更新需要业务侧控制。
压缩大key
对大key进行压缩,可使用各语言提供的压缩方法进行压缩后重新写入,压缩方法需要用户自行调研选择,通常gzip、snappy等都可以。
不读写大key
这种方法不建议使用,不读写大key可以避免大部分问题,用户可避免获取大key 的全部内容,比如避免使用hgetall获取全部内容,以及避免O(n)等开销大的操作。但这样做也只能缓解,无法完全避免大key的影响,在主从同步、故障恢复、数据迁移、扩缩容过程中会传输整个key,所以在这些过程中会引起抖动。
2. 热 key
笔者曾经在刚工作的时候因为不了解原理,搞出过一次愚蠢的在线问题。背景是每一个 query 要在线读取一个 feature,当时做法是,redis 里存一个 hash 类型的对象,key 是 query_feature,value 是一个 docid->feature_value 的 map,这会导致上线后直接把一个单实例打崩。
所以我们明确一下定义:热key是指用户访问一个key的qps特别高,导致Server实例出现cpu负载突增或者不均的情况,热key没有明确的标准,qps 超过500就有可能被识别为热key,监控平台可以监控访问qps top的key。
这个问题最明显的现象是,多个实例的 qps 出现了明显的倾斜现象。
消除热key的最佳实践
设置本地 cache
结合具体业务场景,在访问Redis前,在业务 Client 侧设置本地 cache,降低访问Redis的qps。
拆分热key
结合具体业务场景,将key:value这一个热key复制写入多份,例如key1:value,key2:value,访问的时候访问多个key,但value是同一个,以此将qps分散到不同实例上,降低负载。但是业务侧同时要自行做好一致性的控制。
开启热key承载
增加 proxy 层,来承载热key的突发流量,以更好的保护Server。
Proxy 本地缓存热key,依赖proxy 承载热key流量;原理如下:
- Client 请求查询 key xxx;
- Proxy 透传 xxx 到redis-server;redis-server探测 xxx 达到热key阈值,封禁;返回hot-key-err(使用redis标准协议,无协议修改)
- Proxy 收到 hot-key-err 后,确定该key为热key;变更key名为 @hot-key@xxx 再次请求 redis server;server端识别 @hot-key@ 前缀后剥离该前缀后查询,不触发封禁逻辑,正常返回值Value-yyy;
- Proxy 收到hot-key的返回后缓存到本地后向上返回;
- 后续client再次查询,在有效期内直接查询Proxy后返回;
参考:京东的热 key 承载方案:https://my.oschina.net/1Gk2fdm43/blog/4331985
3. 单线程特点:
单实例,不是说只有一个线程,命令请求、耗时(刷盘)。但是所有的命令请求的处理是在一个事件循环中的。因此,要保证一个命令要尽快被处理。否则后续命令会阻塞。
大key对应一个set要删除时,如果redis版本大于4,可以删,有惰性删除的机制,不然就要自己扫出来subkey,平滑删除。
4. scan优先:http://chenzhenianqing.com/articles/1410.html
5. key的过期删除也是在后台执行的,但是如果同时过期的key太多,也会影响其他操作。
6. O(N)命令关注N的数量。
以List类型为例,LINDEX、LREM等命令的时间复杂度就是O(n)。也就是说,随着List中元素数量越来越多,这些命令的性能越来越差。而Redis又是单线程的,如果出现一个慢命令,会导致在这个命令之后执行的命令耗时也会增长,这是使用Redis的大忌。
7. 禁止危险命令
线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。
8. 多数常规情况下,pipeline比mget的效率天然要好。主要原因:对代理层负载友好。
1. mget在proxy层会做拆包解包,pipeline不需要 直接转发
但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
2. 注意两者不同:
1. 原生(mget/mset)是原子操作,pipeline是非原子操作。
2. pipeline可以打包不同的命令,原生做不到
3. pipeline需要客户端和服务端同时支持。
9. 对于key:保证可读性、可管理、简洁性(不要过长)
10. 对于value:防止大value
11. O(N) 命令
rd 需要自查是否含有 O(N) 命令(如:hgetall、lrange、smembers、zrange、sinter、keys 等),如果有 O(N) 建议严格控制 N 的规模,将单个的返回值控制 100KB 之内,以保证服务稳定性。
常见问题
如果真的需要遍历全集怎么办
可以采用Redis的另一个命令scan。
我们看一下scan的特点:
- 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程
- 提供 count 参数,不是结果数量,是Redis单次遍历字典槽位数量(约等于)
- 同 keys 一样,它也提供模式匹配功能;
- 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
- 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零
解释:
- SCAN命令是增量的循环,每次调用只会返回一小部分的元素。所以不会让Redis假死;
- SCAN命令返回的是一个游标,从0开始遍历,到0结束遍历;