缓存
Redis
功能
- bitmap 支持按bit存储信息,可以用来实现BloomFilter
- HyperLogLog 提供不精确的去重计数功能,比较适合用来做大规模数据的去重统计
- Geospatial 用来保存地理位置,并作为距离计算计算或者半径计算位置等
- pub/sub 订阅发布功能,可以用作简单的消息队列
- Pipeline 批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答
- Redis 支持提交Lua脚本执行功能
- 事务 保证串行执行命令,并且能全部执行,但是名利失败是不回滚。
持久化
RDB
通过fork子进程,在指定的事件间隔内(配置文件中的save参数),把内存中的数据集以快照的形式,采用二进制压缩存储方式写入磁盘。
- 把整个Redis数据保存在单一文件dump.db中,适合做容灾备份
- 服务shutdown会自动持久化
- 输入bgsave会持久化
缺点
- 快照保存完成之前如果宕机,这段时间的数据会丢失
- 保存快照可能导致服务器短时间不可用
AOF
以追加模式将每个更新操作吸入日志文件中。Redis重新启动时读取这个文件,重新执行新建、修改数据的命令恢复数据。
保存策略: 推荐(默认):每秒持久化一次,可以兼顾速度和安全性
缺点
- 相同规模的数据集,AOF比RDB占用更多的存储空间
- 恢复备份数据慢
- 每次读写都同步的话,有性能压力
- 存在个别bug,造成不能恢复
扩容
- 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容
- 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系节点数据一旦确定不能变化。如果Redis节点需要动态变化的话,必须使用可以在运行时进行数据在平衡的一套系统,而当前只有redis集群可以做到
高可用
Redis支持主从同步,提供Cluster集群部署模式,通过Sentinel哨兵来监控Redis主服务器状态,当主服务挂掉后,在从节点中根据选主策略选出新主,并调整其他slaveof到新主
选主策略:
- slave的priority越低,优先级越高
- 同等情况下,slave复制的数据越多优先级越高
- 相同条件下runid越小越容易选中
Sentinel会进行多实例部署,通过Raft协议保证自身高可用
Redis Cluster使用分片机制,在内部分16384个slot插槽,分布在所有master节点上,每个master节点负责一部分slot,数据操作时按key做CRC16计算在哪个slot,由哪个master进行处理。数据冗余通过slave节点保障。
key失效机制
设置过期时间,过期后的key采用定期主动shanc,或在访问时触发被动删除。
过期淘汰策略
- 对于设置了失效期的key做LRU、最小生存时间、随机剔除
- 针对所有的key做LRU,随机剔除
内存淘汰策略
- noeviction:不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,时Redis默认内存淘汰策略
- allkeys-lru:淘汰整个键值中最久未使用的键值
- allkeys-random:随机淘汰任意键值
- allkeys-lfu:淘汰整个键值中最少使用的键值(4.0新增)
- volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值
- volatile-random:随机淘汰设置了过期时间的任意键值
- volatile-ttl:优先淘汰更早过期的键值
- volatitle-lfu:淘汰所有设置了过期时间的键值中最少使用的键值(4.0新增)
内存淘汰算法
LRU(Least Recently Used,最近最少使用)
基于链表结构实现,最新操作的键会被移到表头,当需要进行内存淘汰时,只需要删除链表尾部的元素即可
Redis使用近似LRU算法,在原有数据结构上添加额外字段,用于记录最后一次的访问时间。
LFU(Least Frequently Used,最不常用)
根据总访问次数淘汰数据,与LRU相比,解决了访问频率很低的缓存,只是最近访问了一次就不会被删除的问题。
分布式锁
控制分布式系统之间同步访问共享资源的方式。
分布式锁实现方式
- 基于MySQL的悲观锁实现,使用最少,因为性能不好,容易造成死锁
- 基于Memcached实现,使用add方法实现,如果添加成功则表示分布式锁创建成功
- 基于Redis实现,使用setnx(set if not exists)实现
- 基于Zookeeper实现,利用Zookeeper顺序临时节点实现
数据结构
- string,内部通过SDS(Simple Dynamic String)来存储,SDS类似于Java中的ArrayList,可通过预分配冗余空间的方式减少内存的频繁分配
- list,ziplist(压缩链表):存储在一段连续内存上,存储效率高,但是不利于修改,适用于数据较少的情况;linkedlis(双链表):插入节点效率高,但内存开销大,地址不连续,容易产生内存碎片;quicklist:双向无环链表,每个节点是一个ziplist
- hash,当Hash表中所有key和value字符串长度小于64字节且键值对数量小于512个时,使用压缩链表来节省空间;超过时使用hashtable
- set,内部实现可以是intset或hashtable,当集合中元素小于512且所有数据都是数值类型时,才会使用intset,否则使用hashtable
- sorted set,有序集合,ziplist/skiplist跳表,当有序集合中元素数量小于128个且所有元素长度小于64字节是使用ziplist,否则使用skiplist。
缓存常见问题
缓存更新方式
缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是DB,也可能是远程服务。数据源是DB时,可以在更新DB后字节更新缓存。
当数据源是远程服务时,可能无法及时主动感知数据变更,一般会选择对缓存数据设置失效期,选择失效更新。key不存在或失效时先请求数据源获取更新数据,然后再次缓存,并更新失效期。
但是如果依赖的远程服务在更新时出现异常,则会导致数据不可用。可采用异步更新,当数据失效时,先不清除数据,继续使用旧数据,然后由异步线程去执行更新任务。
数据不一致
产生原因一般是主动更新失败。
解决办法:如果服务队耗时不敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败更新;如果短期数据不一致不会影响业务,则只要下次更新成功,保证最终一致性就可以。
缓存穿透
对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致缓存不命中,会有大量请求穿透缓存访问DB
- 对不存在的用户,在缓存中保存一个空对象进行标记,防止相同ID再次访问DB。可能导致缓存中存储大量无用数据
- BloomFilter过滤器,存在性检测,如果BloomFilter中不存在,那么数据一定不存在;如果BloomFilter中存在,实际数据也可能不存在。
缓存击穿
某个热点数据失效时,大量针对这个数据的请求会穿透到数据源
解决方案
- 使用互斥锁更新,保证同一进程中不会并发请求到DB,减小DB压力
- 使用随机退避方式,失效时随机sleep一个更短时间,再次查询,如果失败在执行更新
- 针对多个热点key同时失效问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点key同一时刻失效
缓存雪崩
缓存挂掉,所有请求会穿透到DB
- 快熟失败的熔断策略,减少DB瞬间压力
- 使用主从模式和集群模式来尽量保证缓存服务高可用