redis相对来讲还是比较简单的,可以简单的认为是简化版本的mysql,一般会有以下几种使用场景:1、持久化,一般用于缓存少量数据;2、二级缓存,同时带ttl时间;3、复杂的检索,通过组合多种数据结构来实现的,一般只是应用在类如消息已读这样的功能中,同时一般会有业务时效的限制,数据不是那么敏感;
这里只强调下上述第一种场景,笔者观点认为用redis做持久化是一种比较糟糕的设计,首先redis主要使用的是内存,如果数据量比较大的话,相比兼价的磁盘来说成本还是比较高的,其次如果数据量不是太多且数据会变化不如采用nacos或zk这样的配置中间件,再次如果数据不会变化比如系统启动参数这些还不如做成本地内存缓存,这样性能会更好。
数据同步
redis的数据主要围绕rdb和aof,如果是单机环境相对比较简单:在服务重启时先加载RDB再加载AOF文件把相关数据同步到内存中,下面会详细介绍下这两个文件在单机环境和master-slave环境中同步的原理。大致的流程如下图所示:
单节点数据同步
- .rdb:快照文件,存放的是数据文件。正常业务系统一般来讲间隔是每60秒创建1个快照,具体要看.conf中的配置和是否调用flushall命令;
- .aof:增量文件,存放的是执行指令。正常来讲每秒会执行久一次,所以理论上在机器出现问题时会丢失1秒钟的数据,这个基本是无解,所以如果读者在面试时被问到这个问题,可以直接告诉面试官如果只有redis做为唯一的存储无特殊设计时这个问题无解,或是采用另外一套补偿、异步等机制重新缓存。
当节点故障重启时redis会先读rdb再执行aof中记录的指令,如果需要同步从节点时,会先同步rdb再同步aof。这两个是互补关系,rdb保证大量数据不会丢失,aof会保证最多丢失1秒的数据,这也是不建议修改appendfsync参数的原因。以上理论理论可以详细参考笔者之前的文章Redis系列(2)- 服务配置 中的详细说明。
RDB方式
Redis使用fork复制一份当前进程,父进程继续接收客户端指令,子进程则把所有数据写入临时文件,写完后替换旧的RDB文件,一次快照操作就完成了。RDB文件是完整的,所以有时我们可以备份此文件来备份数据库。
AOF方式
默认情况下AOF是关闭的,可以通过appendonly yes来开启,每执行一条更改数据的指令,就会把这条指令写到AOF文件中,这个文件的位置和RDB相同,也可以通过dir和dbfilename参数来修改。AOF是个存文本文件,它个文件是由redis通过参数来自动优化的。系统在启动时会执行AOF文件中的指令,来把RDB中数据载入到内存中。
每次执行数据操作时,会有30秒的一个延迟。这些数据在这段时间内会暂放在硬盘缓存中,这时也有可能丢失数据。可以指定appendfsync来改变,everysec(每秒),always(每次执行指令后),no(30秒,系统级别)
验证主从复制完成
一种方法是在向主服务器写入真正的数据后,再向主服务器写入一个虚构值,然后通过验证从服务器是否存在此虚构值来验证数据是否发送完成。另一种节约时间的做法是用INFO aof_pending_bio_fsync如果返回值为0表示写入完成。
多节点数据同步
持久化功能保证了数据的少量损失,但无法避免单点故障。这也就有了master-slave之分,slave节点默认都是只读的,如果直接修改会报错。也可以通过slave-read-only参数修改为可写的,但slave节点的数据不会同步到主数据库可能会出现修改后被覆盖的情况。它接受主数据库同步过来的数据,可以有多个slave节点,但只能有一个master节点。
文件数据同步
首先这是一个异步的过程,slave启动时会向master发送sync指令,master接收后其自身初始化完成后(包括快照内容),会将RDB和AOF中的所有内容发给slave,当主从数据库断开连接后会重新执行上述过程。sync过程中master并不会阻塞,还可以处理客户端发来的命令。可以配置slave-serve-stale-data=no来使从数据库在同步完成前对所有命令回复错误。同步后slave中原有的数据会被清除掉。
因为是一个异步的过程,可能导致主从数据不一致的情况,可以设置master的min-slaves-to-write:3,min-slave-max-lag:10。前一个参数表示只有当至少3个从数据库连接到主库时,主库才可以写,第二个参数表示从数据库最大失去连接的时间。
另外还有一个测试阶段的参数repo-diskless-sync yes来开启无硬盘同步功能,即不写文件直接通过网络发送,减少硬盘性能瓶颈。
增量数据同步
V2.8版本后提供了增量复制功能,不再发送sync命令,而是发送psync命令。原理:在主从同步过程中每同步一条都会放在一个积压队列中,当断线重联后,会判断积压队列中的值如果满足条件则增量复制,否则整体复制。默认情况下积压队列大小为1MB,可以通过repl-backlog-size来调整,数据越大,允许断线的时间越长。另一个配置项是repl-backlog-ttl当断线后,经过多长时间可以释放积压队队的空间,默认为1小时。
性能优化
如何查找性能问题
redis本身使用IO多路复用,例如epoll (提升吞吐量),纯内存操作 (处理请求耗时短,单位时间内处理的请求数量就多),高效的数据结构,比如hash等 (尽量接近O(1)时间复杂度高效查询)等来保证性能,如果遇到性能问题时多数是以下两种原因引起的:
- 内存,因为一般情况来讲在现有分布式环境中cpu一般都不是瓶颈,redis的单线程设计也是为了提高性能节省线程上下文的切换的时间;
- 连接池,客户端未使用pool或使用不当导致持续的创建和关闭带来的损耗;
提高吞吐量的方式
- 管道技术:因其本身是单线程设计,所以管道(pipeline)在某些场景下非常有用,比如有多个操作命令需要被迅速提交至服务器端,但用户并不依赖每个操作返回的响应结果,对结果响应也无需立即获得,那么管道就可以用来作为优化性能的批处理工具。性能提升的原因主要是减少了 TCP 连接中交互往返的开销。
- 分片技术:原生的redis由于集群hash分片原理。具体的redis命令,会根据key计算出一个槽位(slot),然后根据槽位去特定的节点redis上执行操作。那么pipeline中每个单独的操作,需要根据“key”运算一个槽位(JedisClusterCRC16.getSlot(key)),然后根据槽位去特定的机器执行命令。也就是说一次pipeline操作会使用多个节点的redis连接,而目前JedisCluster是无法支持的,如果想使用这种技术,需要自行根据key计算出通道,每个管道连接单独的节点,这样就不会有问题了。
- 多线程:并行化子流程,这是一种业务端的改造,把串行流程改成并行流程,但需要注意这需要仔细分析下业务需求再进行改造;
读性能提升
除了上面提到的master-slave以及shard技术,还有一种方式编程方式也可以提升读的性,就是在连接时使用带ssh隧道(tunnel)会明显降低带宽,但同时也要考虑加密和压缩开销问题。单核CPU使用AES-128算法每秒可加密180M数据,使用RC4算法会达到350M左右。同时ssh默认采用gzip压缩协议,这个协议可以设置压缩级别,一个可参考值是1级20~50M/秒,建议用5级以下,会缩小20%左右,处理时间会比1级慢2~3倍;
节省内存
redis是基于内存/硬盘的数据库,内存的数量是有限的,所以有时还需要采用一些措施减小内存的占用:
精简KV
key缩小的一个实现方案
用10进制代替16进制等。在运算时再翻译成2进制,这样会减少碰撞的机率,下面的例子是一个设计思路,比如
- UUID=128位数字,占用32字节(字符串)
- 取uuid前15位转换为10进制进行存储,则会占用8个字符(注意redis最大只能存64位有符号整数)
- 15位是否够用?把uuid翻译成二进制,则前15位就是56个二进制位,碰撞的机率是当数量超过2.5亿时,重复的机率是1%。
value缩小的一个实现方案
这种方式类似于用户地址信息的记录方式,首先地址信息是个字典表,然后真实数据就是一个结构化的串,最后打包存储起来。但这样不适合like查询。
短结构
redis为List, hash, sortset提供了一组配置,用一种短结构替换原来的数据存储结构,称为压缩列表。比如list原有的结构是一个双向列表,它由三部分组成双向指标+数据,其中数据又分三部分,字符串长度占用的字节数+可用的字节数+字符串长度。经过ziplist后其结构为去掉了指标信息,把数据部分压缩成两部分:前一节点的位置长度+当前数据,在查找时就会根据这个长度来取得不同的数据。这样会节省3倍左右空间。正常来讲redis实际占用的空间是数据的7倍左右。
list-max-ziplist-entries 512
list-max-ziplist-value 64
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
zset-max-ziplist-entries 512
zset-max-ziplist-value 64
//允许最大的元素512个(正常上限1024,否则会影响性能),每个节点最大64个字节,可用debug命令查看是否启用了压缩,如果超过了上面限制就会采用原来的数据结构。
set-max-intset-entiries 512 //这个集合只支持整数
分片结构
这个技术其实是和短结构共同使用来压缩内存的,在使用前要计算数据的散列性是否均衡,否则起不到太大的效果。其key由原来的Y被设计成Y:<shardid>。如果key为数字可以取模也可以通过crc32算法来计算后再取模,这个算法要比md5或sha1算法快。