Redis数据类型
- string
- hash
- list
- set
- sorted set
- bitmap
- hyperloglog
- geospatial index
Redis并没有使用固定的数据结构来存储各种类型的数据,而是创建了一套对象系统,对于同一个对象,可以对应一个或多个不同的底层数据结构(或者叫做编码方式),某些特定的编码方式在时空间的效率上有所优化,通过执行”Object Encoding”可以查询当前编码方式。
- String 字符串对象,最大可支持512MB,memcache最大只支持1MB。编码可以是int,raw或者embstr,int对应整型数据,可方便计数,embstr用来保存长度小于等于39字节的字符串值,采用连续的空间进行存储,更好利用缓存优势;字符串对象常用来进行计数,或者缓存序列化的对象;
- List 列表对象,编码可以是ziplist或linkedlist,ziplist是为了节约内存而开发的,是一个经过特殊编码的连续内存块组成的顺序结构,当列表对象元素的个数较少以及元素的长度较短时会采用这种方式;列表对象一般可用来实现消息队列;
- Hash 哈希对象,编码可以是ziplist或hashtable,在Redis的实现里,采用的链式冲突来解决冲突问题,并且为了维护hash表的负载因子在一个合理的范围,会执行渐进式rehash;哈希对象一般用于存储某个对象的属性数据,便于选择性查询,这个效率要比粗暴的序列化和反序列化要高很多,比如用户的个人资料;另一个用法,则是利用ziplist编码方式实现压缩存储,节省内存;
- Set 集合对象,编码可以是intset或hashtable,当集合的元素不多且都是整数时,Redis就会使用整数集intset,底层是一个以有序、无重复的方式进行排列的数组,能有效的节约内存;这个对象一般用于去重,比如派奖;
- Sorted Set 有序集合对象,编码可以是ziplist或者skiplist,跳跃表skiplist是一种查找效率可媲美平衡树的数据结构,平均O(logN),最坏O(N),而且实现更加简单;其实,Redis用了skiplist和hashtable两种数据结构来实现zset,一方面hashtable能实现O(1)的查找,另一方面skiplist实现了有序,可支持范围查找;有集合序对象用的就比较广泛了,比如排行榜(只要是排序相关的列表都可以)、延迟任务队列等。
缓存淘汰策略
高性能
- Redis虽然是单进程单线程模型,但是读写性能非常优异,单机可支持10wQPS,原因主要有以下几点:
- 纯内存操作,避免了与磁盘的交互
- 用hash table作为键空间,查找任意的key只需O(1)
- 单线程,天生的队列模式,避免了因多线程竞争而导致的上下文切换和抢锁的开销
- 事件机制,Redis服务器将所有处理的任务分为两类事件,
- Redis提供了RDB和AOF两种持久化方式。
- RDB:会生成一份内存快照—RDB文件,该文件是经过压缩的二进制格式,记录的是键值对数据;
- AOF:是以Redis的命令请求协议格式来保存,记录的是命令操作;
- RDB的特点,文件体积小,加载速度快;但因为是对整个实例的内存生成快照,所以操作比较重,一般持久化的间隔不宜太快,所以保存的数据相对比较旧一些;
- AOF的特点,文件体积较大(可以用AOF重写进行覆盖);所有的写操作会追加到AOF缓冲区,持久化的行为可配置,分为三种,always(每次刷盘)、everysec(异步线程每隔1秒刷一次)和no(只写到page cache,交给操作系统来刷盘);相对来说,AOF文件数据保存的比较新一些,所以如果开启了AOF,那么Redis服务器恢复的时候会优先加载AOF文件。
由于RDB SAVE和AOF重写会阻塞主线程,所以都支持BG模式执行,至于持久化的具体实现这里就不展开讨论了。
- 支持复制、Lua脚本、LRU淘汰、事务
高可用
- Redis的高可用,主要通过主从复制机制以及Sentinel集群来实现。
- 主从复制 分为两个阶段,首先,当从服务器发起SYNC命令后,主服务器会生成最新的RDB文件发送给从服务器,并使用一个缓冲区来记录从此刻开始主服务器执行的所有写命令;待RDB文件传输完之后,再将该缓冲区的数据再发送给从服务器,这样就完成了复制。旧的Redis版本有个缺陷是,如果在第二个阶段发生失败,需要从第一个阶段重新开始同步,而这个阶段的操作会消耗大量的CPU、内存和磁盘I/O以及网络带宽资源,太过耗费资源。所以从2.8版本开始,实现了部分重同步,通过主从服务器各维护一个复制偏移量来实现。
- Sentinel 由一个或多个Sentinel实例组成的哨兵系统,可以监视任意多个主从服务器,并完成Failover的操作。Sentinal其实是一个运行在特殊模式下的Redis服务器,运行期间,会与各服务器建立网络连接,以检测服务器的状态;同时会与其它Sentinel服务器创建连接,完成信息交换,比如发现某个主服务器心跳异常时,会互相询问心跳结果,当超过一定数量时即可判定为客观下线;一旦主服务器被判定为客观下线状态,那么Sentinel集群会通过raft协议选举,选出一个Leader来执行Failover。
- Failover 一般来说,会先选出优先级最高的从服务器,然后再从中选出复制偏移量最大的实例,作为新的主服务器;最后将其它从和旧的主都切换为新主的从。
当从服务器有2个或者多个时,Redis的主从架构可以有两种形式。一种是,所有的从服务器直接挂在主服务器上,这种模式的优点是,所有从服务器复制的延迟相对较低,而缺点在于加大了主服务器的复制压力;另一种形式,是采用级联的方式,S1从M复制,S2从S1复制,以此类推,这种模式的优点是,将主服务器的复制压力分摊到多个服务器上,而缺点在于越处于级联下游的从实例,复制延迟就越大。
从主从复制模式可以看出,Redis的数据只能保证最终一致,不能保证强一致性。
分布式
- 读扩展,基于主从架构,可以很好的平行扩展读的能力。写扩展,主要受限于主服务器的硬件资源的限制,一是单个实例内存容量受限,二是一个实例只使用到CPU一个核。下面讨论基于多套主从架构Redis实例的集群实现,目前主要有以下几种方案:
- 客户端分片 实现方案,业务进程通过对key进行hash来分片,用Sentinel做failover。优点:运维简单,每个实例独立部署;可使用lua脚本,业务进程执行的key均hash到同一个分片即可;缺点:一旦重新分片,由于数据无法自动迁移,部分数据需要回源;
- Redis集群 是官方提供的分布式数据库方案,通过分片实现数据共享,并提供复制和failover。按照16384个槽位进行分片,且实例之间共享分片视图。优点:当发生重新分片时,数据可以自动迁移;缺点:客户端需要升级到支持集群协议的版本;客户端需要感知分片实例,最坏的情况,每个key需要一次重定向;不支持lua脚本;不支持pipeline;
- Codis 是由豌豆荚团队开源的一款分布式组件,它将分布式的逻辑从Redis集群剥离出来,交由几个组件来完成,与数据的读写解耦。Codis proxy负责分片和聚合,dashboard作为管理后台,zookeeper做配置管理,Sentinel做failover。优点:底层透明,客户端兼容性好;重新分片时,数据可自动迁移;支持pipeline;支持lua脚本,业务进程保证执行的key均hash到同一个分片即可;缺点:运维较为复杂;引入了中间层;
使用错误场景
- 键过大
Redis的key是string类型,最大可以是512MB,那么实际中是不是也可以这样用呢?答案是否定的,redis将key保存在一个全局的hashtable,如果key过大,一是占用过多的内存,二是计算hash和字符串比较都会更耗时;一般建议key的大小不超过2kB。
- Big key
或者说是big value,这会导致删除key的操作比较耗时,会阻塞主线程。比如有些同学喜欢用集合类的对象,动辄上百万的元素。对于这类超大集合,一般有两种优化方案,一是采取分片的方式,将每个集合分片控制在较小的范围内,比如小于1000个元素;二是起一个异步任务,对集合中的元素分批进行老化。
- 全集合扫描
比如在业务代码使用了keys*,hgetall,zrange(0, -1)等返回集合中所有元素,这些都属于阻塞操作,一般考虑用scan,hscan等迭代操作代替。
- 单个实例内存过大
内存过大有什么问题呢?上文中在讲到持久化的时候其实有说到,无论是生成RDB文件,还是AOF重写,都是要对整个实例的内存数据进行扫描,非常消耗CPU和磁盘资源;当使用Backgroud方式创建子进程时也会涉及到内存空间的拷贝,即便使用了COW机制,也会占用相当的内存开销。另外,在主从复制的第一阶段,save、传输和加载RDB文件的开销,也会随着RDB文件的变大而变大。当单个实例达到瓶颈时,更好的解决方案应该是采用集群方案。
- 大量key同时过期
redis删除过期键采用了惰性删除和定期删除相结合的策略,惰性删除则是在每次GET/SET操作时去删,定期删除,则是在时间事件中,从整个key空间随机取样,直到过期键比率小于25%,如果同时有大量key过期的话,极可能导致主线程阻塞。一般可以通过做散列来优化处理。