- 字符串 string;
- 作为计数器,比如点赞数、阅读数、转发数、视频或直播在线观看人数等;
- 分布式 ID,每次从 redis 中 incr 获取加一的 ID;
- session 管理,每次用户更新或查询登录都直接从 redis 中获取;
- 限速、限流,比如 3 分钟只能获取一次验证码、一分钟内只能请求 10 次等。
- 列表 List,作为消息队列或栈;
- 集合 Set;
- 求两个集合的交集,可作为共同关注、好友等;
- 秒杀集合,结合发布订阅,将请求添加到集合中,进行消费为其创建订单,集合长度超过秒杀数量则返回秒杀失败。
- 哈希表 Hash,缓存对象信息,比如用户信息、购物车信息等;
- 有序集合 Zset;
- 排行榜;
- 延时队列,value 为序列化的任务消息,score 为下一次任务消息运行的时间。轮询 zset 中 score 值大于 now 的任务消息进行处理;
- 服务发现,value 为服务地址,score 为心跳时间。每隔段时间调用 zadd 保持心跳。服务变更时,递增版本号,消费者通过轮询版本号来重加载服务。
- Geospatial,以给定经纬度为中心,找出某一半径内的元素,可获取身边的人;
- Hyperloglog,基数统计算法,统计 UV、日活、月活等;
- 这类问题是统计一个集合中不重复的元素,在数据量小的情况下可用常规方法,比如集合 Set、位图 bitmap、布隆过滤器来做;
- 集合 Set,统计一亿个不重复元素,每个元素 4 字节,则需要 400MB;
- 位图 bitmap,每个 bit 用于判断该元素是否出现过,统计一亿需要 100000000 * 1bit / 8 ≈ 12MB;
- 布隆过滤器,占用的内存比位图小,但存在误判;
- Hyperloglog,牺牲一定准确性,但占用内存特别少,只有 12kb,适合大数据统计。
- 位图 Bitmap。
- 作为签到、是否登录等;
- 布隆过滤器,redis 4.0 后不用位图,官方有提供。
- 分布式锁。
string 结构,最简单的结构,使用 incr 进行加一。公众号文章、微博,都有阅读数。
使用列表对象实现,列表是双端队列,可实现队列和栈,分为 Lpush、Lpop、Rpush、Rpop。
队列,即模仿 MQ 的入队和出队。
使用 redis 集合实现,两个集合的交集即可得出共同关注、好友等。
- 差集,SDIFF key1 key2;
- 交集,SINTER key1 key2;
- 并集,SUNION key1 key2。
部分内容来自 [WeiDesign]微博计数器的设计(下),介绍了在大数据量下,如何对 redis 存储进行优化,从 key 和 value 两方面入手。
用到的方法有,一是为 0 的都不存储,查询不到即返回 0。二是 key 和 value 使用数组,在进行压缩。三是根据 ID 的时间属性区分冷、热数据,冷数据存到磁盘。
- MySQL 实现,维护三张表;
- 表为用户 ID + 文章 ID,保存每个人的点赞信息,判断该用户是否点赞过某文章,获取某文章的点赞用户列表;
- 文章点赞数表,key 为文章 ID,获取文章点赞数;
- 用户点赞数表,key 为用户 ID,获取用户点赞数。
- redis 实现,使用 string + set;
- string,get 用于获取点赞数量,incr 点赞数加一操作;
- 还可以使用 hash 来存储,创建多个当天的 hash,每个 hash 存储一定数量的文章 ID 和点赞值。hash 名包含时间属性,可根据时间远近,区分冷、热数据,将时间久的 hash 删去缓存,有查询时从数据库中获取;
- set,集合判断该用户是否已对某文章点赞。
- 微博的计数器基于 redis 二次开发,主要在存储方面进行优化;
- 为 0 的数据不存储,不存储是种默认值,表示返回 0,可节省一半内存;
- 两个数组,key 为微博 ID 数组,value 为对应微博 ID 数组的评论数和转发数的数组,比如微博 ID 数组为 [35, 36],value 为 [[464, 239], [10, 15]],微博 ID 为 35,其转发数和评论数分别为 464、239 等。
- 将两个数组进行 LZF 压缩,再存储,节省空间;
- 微博 ID,应包含时间属性,可根据时间来区分冷、热数据,冷数据存在磁盘,热数据存在内存,使用定时器来清除 90 天以前的数据。
最简单的,使用 string 的 incr。每次获取全局业务 ID,到 redis 中进行 incr,操作成功,则使用该 ID。缺点是业务量大,频繁请求,网络开销大。
第二种是从 redis 中获取一批 key 进行操作,使用 string 的 incrby,一次性增加 2000 等。incrby 成功后,在本地机器 A 发放 1000 ~ 3000 的用户 ID。即使本地缓存丢失,就浪费一批 ID,可重新获取新的一批。
如果有多台机器来作为分布式 ID 集群,比如 3 台机器,每台机器的初始 ID 为 1,2,3,每次 incrby 3 即可,即每次增加步长为 3。
以购物车为例,使用哈希表 hash 来实现,key 格式为 cart:购物车 ID,比如 cart:3234,购物车 ID 为 3234 的哈希表。
哈希表适合对象存储,string 适合字符串存储。
两个 redis 集合 set,一个 setA 查询该用户是否已秒杀过,另一个 setB 为已被秒杀商品的集合,如果该 setB 长度超过指定的秒杀数,则表示抢完。
用户抢购秒杀商品时,是将消息推送到另一个频道,由该频道将用户 ID 添加到已被秒杀商品的集合 setB。另外一个线程,处理 setB 中的元素,为其创建订单记录,创建成功,再将消息推送到另一个频道,由前端解析消息,告知用户秒杀成功。
- 先到 redis 的用户集合 map 中,查询该用户是否已经抢过,抢过则返回;
- 通过秒杀活动 ID,查询活动商品表是否存在秒杀商品。不存在,则返回该秒杀活动不存在;
- 存在,根据用户 ID,查询秒杀活动订单表。
- 存在表示该用户已经抢到商品;
- 不存在,获取则获取活动商品表的数量,如小于 0,表示商品已抢完。
- 拼接 key + 活动 ID,获取秒杀商品集合 map。判断该 Map 的长度,如果长度超过秒杀数量,表示抢完,则直接返回;
- Map 长度没超过秒杀数量,则包装成消息,发送到 redis 的订单频道 orderChannel;
- redis 的订单频道 orderChannel 接到消息后,解析消息,将用户 ID 和活动 ID 添加到秒杀集合中;
- 定时任务,定时遍历秒杀集合,为集合中的用户创建秒杀订单记录,同时会将该用户添加到用户集合,防止同一个用户多次抢购;
- 创建完订单记录,商品数量减一后,会发送消息到 imChannel。由前端解析该消息,推给用户表示已经抢购成功。会有个订单编号,用户点击跳转到订单详情页面完成后面的支付逻辑。
有序集合 Zset 实现排行榜,消息排序(根据消息权重区分重要消息、普通消息来排序)等。
比如统计主播的收益排行榜,主播 id 作为 member,当天打赏的活动礼物对应的热度值作为 score, 通过 zrangebyscore 获取主播活动日榜。
内存占用小,解决缓存穿透的方法。 缓存穿透指查询不存在的值,会去存储层查询。大流量情况下,会对存储层造成大的负担。
它由一个 bit 数组和一组 Hash 算法构成,可用于判断一个元素是否在一个集合中。 它是将所有地址经过多个 Hash 算法,映射到一个 bit 数组,其数学原理在于两个完全随机的数字相冲突的概率很小。
- 使用 jedis 客户端,自己实现分布式锁;
- Redission 封装了分布式锁的实现,可直接调用;
- RedLock,解决主从的锁同步问题。
- 比如机器 A 向主机器申请锁,这锁没有同步到 B 机器,然后主机器宕机了。这时 B 机器去从机器能申请到锁;
- RedLock 解决方法是使用少数服从多数原则,机器 A 申请锁时,是向多个 redis 机器申请锁,当获得多数锁时,才成功获取锁,适用于更严格的场景。
Geospatial 属于 redis 的特殊数据类型,以给定经纬度为中心,找出某一半径内的元素。
GEO 底层实现原理是 Zset,使用 Zset 命令来操作。
- geoadd china:city 116.40 39.90 beijing 114.05 22.52 shengzhen,添加,需要经纬度;
- geopos china:city beijing,获取指定 key 的经纬度;
- geodist china:city beijing shengzhen km,获取两地的距离;
- georadius weixin:person 115 20 10km count 100,查询经纬度 115、20 为中心,寻找 10km 以内的人,筛选获取 100 个。
Hyperloglog 属于 redis 的特殊数据类型,作为基数统计算法,可用于估计网页的 UV。
对比使用 set 来保存用户 ID,Hyperloglog 优点是占用内存固定,只要 12 kb,缺点是为估计,只能计数,且有 0.81% 错误率。
如果需要精确统计,并获得每个人的 ID,使用 set。
只有两个状态 0 和 1,非常省内存。可用为统计用户信息,比如签到、是否登录等。
- setbit 标识:日期 uid 1,比如 uid 为 546 的用户在 2020.03.15 签到,则为 setbit SignIn:20200315 546 1;
- getbit 标识:日期 uid,获取某位用户是否签到,比如 setbit SignIn:20200315 546;
非原子性的,有一条命令失败,也会执行下去,本质是将多条命令放到一起执行。
- MULTI 命令,表示事务开始。这时开始输入其他命令,会进入 redis 的命令事务队列;
- EXEC 表示事务执行,redis 会执行事务队列中的命令;
- DISCARD 表示丢弃事务队列中的所有命令,Redis 不支持回滚操作。
WATCH 命令是一个乐观锁,原理为 CAS,检查被监视的键是否有修改过,是则拒绝执行事务。
将内存的数据保存到磁盘中,防止机器意外断电、重启导致数据丢失。Redis 有两种持久化方法:
- RDB 持久化,快照形式,全量备份。生成一个压缩的二进制文件 RDB 文件,保存的是数据库的键值对数据;
- AOF 持久化,增量备份。每隔一段时间(比如每隔一秒)保存 Redis 服务器所执行的写命令。
Redis 的过期策略为定期删除 + 惰性删除。注意,为防止大批 key 同时过期,导致 redis 频繁扫描影响性能。建议将过期时间随机化,防止同时过期。
从库不会进行过期扫描,而是进行主从同步,存在主从延迟情况。
- 定时删除(主动删除),对内存友好,但对 CPU 不友好。在 CPU 使用高峰时,执行定时删除,会影响其性能。同时,创建定时器用到 Redis 服务器的时间事件,它是使用无序链表,导致查找一个事件的时间复杂度为 O(N);
- 惰性删除(被动删除),对 CPU 时间最友好,但对内存最不友好。 获取键时,检查是否过期,过期则删除。如果有很多键没有访问获取(数据太旧),则没法删除,相当于内存泄漏;
- 定期删除(主动删除),前两种的折中。 设定删除操作执行的时长,每隔一段时间,检查一次并删除。
当 Redis 的内存超过期望内存 maxmemory 时,会执行内存淘汰机制。Redis 的过期机制有可能会导致内存泄漏,需要对 key 执行淘汰策略。
常用的有近似 LRU 算法,Redis 随机抽取 5 个 key,检查这 5 个 key 最后一次的访问时间戳,丢弃最旧的那个,一直重复该流程。
- 用户消费的数据远大于生产的数据,比如热卖商品或秒杀商品、热点新闻、热点评论等,这些典型的读多写少的场景会产生热点问题;
- 某个 key 的请求超过一台单机的性能极限,假设一台单机能承受 3000 请求,这时有 5000 请求查询,也为热点 key。
- redis 集群扩容,增加分片副本,均衡读流量;
- 对于超过 redis 单机的性能极限的,将热 key 进行备份,分成 key1、key2 ... 等,分到不同分片,访问时随机选一个,分散流量;
- 同时将热 key 缓存到本机,减少 redis 请求。
使用 redis-benchmark,比如 redis-benchmark -h localhost -p 6379 -c 100 -n 100000,表示测试 redis 地址为 localhost:6369,100 个并发连接,100000 个请求。
- 缓存击穿,热点 key 过期,导致大量请求打到数据库。
- 使用分布式锁,保证只有一个请求到数据库获取,接着存到 redis 中;
- 不让热点 key 失效,先识别出,并主动将其存到 redis 中。
- 缓存穿透,查询不存在的数据,直接穿透到数据库查询;
- 布隆过滤器;
- 回种短期空值。
- 缓存雪崩,大量 key 同时过期。
- 设置 key 的过期时间时在加上个随机值;
- 限流,降级,比如用 Hystrix;
当一个 key 用 set 设置过期时间后,在重新设置不带过期时间,会导致过期时间消失。
在项目中需要合理评估 redis 容量,避免频繁 set 导致没有过期策略,间接导致内存溢出。
Jedis 调用 expireAt 命令时有 bug,最终调用的是 pexpire 命令,这个 bug 会导致 key 过期时间很长,出现内存溢出。解决方法,是升级。
RedisTemplate 在执行 execute 方法,会获取连接。当选择库大于 1 时,会调用 select db 进行切换。
注意,执行完 execute 方法后,会释放连接时,redis 会将库重新切换为 0。即每次连接会从 0 切换到另一个库,执行完后又切换回 0 库,这样切换损耗性能。
Rediscluster 集群数据库,默认 0 库,无法选择其他数据库。
redis 是单线程的,而这种命令,其执行时间与集合的长度有关,如果集合长度非常长,会占用 redis 唯一个线程来进行处理。
这时在出现高并发查询该集合,导致每个请求变长,CPU 涨到 100%,阻塞了其他命令执行。建议 set、hash、list 等集合中的元素个数不超过 1000。
比如查询榜单,一次查询日榜、周榜、月榜等,同时返回三个榜单数据。对比三次查询,更优。
- Monitor 命令,在高并发下,会存在内存暴增和影响 Redis 性能的隐患;
- keys 命令,会遍历所有 key,前面提到 O(N) 下,如果 key 非常多,会阻塞其他命令;
- Save 命令,会阻塞当前 redis 服务器,先进行持久化,直到持久化完成;
- BGREWRITEAOF 命令,手动 AOF,同样对内存较大的实例会造成长时间的阻塞。
- slowlog get 5,获取慢查询日志;
- CacheCloud 工具监控
redis 是单线程,一般使用率为 10%,如高于 20%,需考虑进行 RDB 持久化。
使用命令 redis-cli -p{port} -h{host} --stat 获取 Redis-cluster 每个分片 requests 流量均衡情况,超过 12W 需告警。
定时扫描大 key,进行优化。命令为 redis-cli -h 127.0.0.1 -p {port} --bigkeys 或 redis-memory-for-key -s {IP} -p {port} XXX_KEY。
Info memory 命令查看,避免在高并发场景下,由于分配的 MaxMemory 被耗尽,带来的性能问题。
重点关注 used_memory_human 配置项对应的 value 值,增量过高时,需要重点评估。