Java组件总目录
Redis Cluster 集群与性能调优
一 Redis-cluster 集群
实现Redis 的内存扩展功能。
1.1 RedisCluster 集群创建
Redis集群最少需要三台主服务器,三台从服务器。端口号分别为:8001~8006, 创建8001实例,并编辑redis.conf文件,修改port为8001。
第一步:创建8001实例,修改redis.conf配置文件,打开cluster-enable yes
第二步:复制8001,创建8002~8006实例, 注意端口修改 。
第三步:启动所有的实例
第四步:创建Redis集群 --cluster-replicas 1 表示每个节点的副本数为1。cluster 从节点默认不可写不可读仅仅是备份。
# 创建集群
./redis-cli --cluster create 127.0.0.1:8001 127.0.0.1:8002 127.0.0.1:8003 127.0.0.1:8004 127.0.0.1:8005 127.0.0.1:8006 --cluster-replicas 1
# 连接到其中一个客户端
./redis-cli –h 127.0.0.1 –p 8001 –c
# 查看集群状态
127.0.0.1:8006> cluster info
# 查看集群中的节点
cluster nodes
1.2 工作原理
下图是6个节点,无备份的cluster集群。
-
所有的redis节点彼此互联( PING-PONG机制 ),内部使用二进制协议优化传输速度和带宽.
-
客户端与Redis节点直连,不需要中间Proxy层,直接连接任意一个Master节点
-
redis-cluster把所有的物理节点映射到 [0-16383] slot 上,cluster 负责维护node→ slot→ value,根据公式HASH_SLOT=CRC16(key) mod 16384,计算出映射到哪个分片上,然后Redis会去相应的节点进行操作。 实现扩容功能。
-
节点的fail是通过集群中超过半数的节点检测失效时才生效
-
当一个主节点不可用时,该机器的备份节点转为主节点,若无备份节点。
节点说明
Redis 集群中内置了 16384个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis
先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。 机器的哈希槽变动时,响应数据也应一起变动。
1.3 Redis-cluster 投票:容错
节点失效判断
- 集群中所有master参与投票,如果 半数以上master节点 与其中一个master节点通信超过(cluster-node-timeout) ,认为该master节点挂掉.
集群失效判断
什么时候整个集群不可用(cluster_state:fail)?
- 如果集群任意master挂掉,且当前master没有slave,则集群进入fail状态。也可以理解成集群的[0- 16383]slot映射不完全时进入fail状态。
- 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。
1.4 优缺点
优点:
- (1)无需Sentinel哨兵监控,如果Master挂了,Redis Cluster内部自动将Slave切换Master
- (2)可以进行水平扩容
- (3)支持自动化迁移,当出现某个Slave宕机了,那么就只有Master了,这时候的高可用性就无法很好的保证了,万一Master也宕机了,咋办呢? 针对这种情况,如果说其他Master有多余的Slave ,集群自动把多余的Slave迁移到没有Slave的Master 中。
缺点:
- (1)批量操作是个坑
- (2)资源隔离性较差,容易出现相互影响的情况。
1.5 主从切换
当集群中节点通过错误检测机制发现某个节点处于fail状态时,会执行主从切换。Redis 还提供了手动切换的方法,即通过执行 cluster failover 命令
1.6 副本漂移
假设A发生故障,主A的A1会执行切换,切换完成后A1变为主A1,此时主A1会出现单点题。
在周期性调度函数 clusterCron中会定期检查如下条件
- 是否存在单点的主节点,即主节点没有任何一台可用的从节点
- 是否存在有两台及以上可用从节点的主节点
如果以上两个条件都满足,从有最多可用从节点中选择一台从节点执行副本漂移。选择标准为按节点名称从小到大,选择最靠前的一台从节点执行漂移。
具体漂移过程
- 从C的记录中将C1移除
- 将C1所记录的主节点更改为A1 5. 在A1中添加C1从节点
- 将C1的数据同步源设置为A1
漂移过程只是更改一些节点所记录的信息,之后会通过心跳包将该信息同步到所有的集群节点。
1.7 扩展集群
当集群需要扩容时需要向集群中添加新的节点。
我们首先创建一个新的redis实例,端口为8007。其他配置同上。
启动7007节点,然后使用redis-cli将新的节点添加到集群中。
1 添加节点
./redis-cli ƕcluster add-node 新节点的ip:端口 现有集群ip:端口
2 分配slot
我们可以看到新的节点已经添加到集群中了,但是新的节点中并没有任何槽在其上,也就意味着,不可能有数据放到这个节点上。那么我们需要将一部分槽移动到新的节点上。
./redis-cli ƕcluster reshard 192.168.133.22:8001
--cluster-from f3ab5f5c53b82b8a9df24bfd4a8191224cf63597,faa53cbb6bae288d7f3a19fa55f18d7e77c9 7f6d, d33eab99055e05440a6ba1dd314efb0a25274fb1
--cluster-to 90dcd8e79fd753d24c35ef6ec63e65d79f56600a 3000
ƕcluster-from:表示slot目前所在的节点的node ID,多个ID用逗号分隔
ƕcluster-to:表示需要新分配节点的node ID(貌似每次只能分配一个)
ƕcluster-slots:分配的slot数量
1.8 添加从节点
我们向集群中添加了一个新的节点7007,如果只有一个主节点的情况下,高可用性会下降。那么我们就应该给这个节点增加一个备份节点提高可用性。我们再添加一个节点7008,按照之前的配置方法配置,然后启动。
./redis-cli --cluster add-node 192.168.133.22:8008 192.168.133.22:8007 -- cluster-slave
--cluster-master-id 90dcd8e79fd753d24c35ef6ec63e65d79f56600a
- add-node: 后面的分别跟着新加入的slave和slave对应的master
- cluster-slave:表示加入的是slave节点
--
cluster-master-id:表示slave对应的master的node ID
1.9 收缩集群
下线节点192.168.23.129:7007(master)/192.168.23.129:7008(slave)
1 首先删除master对应的slave
./redis-cli ƕcluster del-node 192.168.133.22:8008 5e8924bef3cfb0a07838d45da77e9a2e0e61d7b9
del-node后面跟着集群中的任意节点的ip及端口号(主要目的是连接到集群,ip:port) 和node ID
2 清空master的slot
reshard子命令前面已经介绍过了,这里需要注意的一点是,由于我们的集群一共有四个主节点,而每次reshard只能写一个目的节点,因此以上命令需要执行三次( ƕcluster-to对应不同的目的节点)。
– cluster-yes:不回显需要迁移的slot,直接迁移。
./redis-cli --cluster reshard 192.168.133.22:8007 --cluster-from 90dcd8e79fd753d24c35ef6ec63e65d79f56600a --cluster-to f3ab5f5c53b82b8a9df24bfd4a8191224cf63597 --cluster-slots 3000 --cluster-yes
3 下线(删除)节点
./redis-cli ƕcluster del-node 192.168.133.22:8007 90dcd8e79fd753d24c35ef6ec63e65d79f56600
二 Redis数据存储的细节
2.1 内存结构
2.2 内存分配器
Redis在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc。
2.3 redisObject
Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。
redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要
redisObject支持,下面将通过redisObject的结构来说明它是如何起作用的。
2.4 SDS
Redis没有直接使用C字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。
三 Redis的对象类型与内存编码
四 Redis 设计优化
4.1 估算Redis内存使用量
下面以最简单的字符串类型来进行说明。
4.2 优化内存占用
4.3 查看Redis内存统计
info memory
used_memory
- used_memory used_memory 字段数据表示的是:由Redis分配器分配的内存总量,以字节(byte)为单位。
- used_memory_human只是显示更加人性化。
used_memory_rss
记录的是由 操作系统分配 的 Redis进程内存 和Redis内存中无法再被jemalloc分配的 内存碎片 (单位是字节)。
used_memory和used_memory_rss的区别:
前者是从Redis角度得到的量,后者是从操作系统角度得到的量。二者之所以有所不同,一方面是因为内存碎片和Redis进程运行需要占用内存,使得前者可能比后者小。
mem_fragmentation_ratio
内存碎片比率 ,该值是used_memory_rss / used_memory的比值。
mem_fragmentation_ratio一般大于1,且该值越大,内存碎片比例越大。
mem_fragmentation_ratio<1:(刚运行时redis内存中无数据 使比例小于1,可查看长期后的数据)
mem_fragmentation_ratio值计算为进程的内存驻留集大小(RSS,由OS测量)与Redis使用分配器分配的总字节数之间的比率。 现在,如果使用libc分配更多内存(与jemalloc,tcmalloc相比),或者在基准测试期间系统上的某些其他进程使用了更多内存,则可以通过操作系统交换Redis内存。它会减少RSS(因为Redis内存的一部分不再存在于主内存中)。由此产生的碎裂率将小于1。 换句话说,只有当您确定操作系统没有交换Redis内存时,此比率才有意义(如果不是这样,那么无论如何都会出现性能问题)。
一般来说,mem_fragmentation_ratio在1.03左右是比较健康的状态(对于jemalloc来说);刚开始的mem_fragmentation_ratio值很大,是因为还没有向Redis中存入数据,Redis进程本身运行的内存使得used_memory_rss 比used_memory大得多。
五 Redis 性能优化
- 设计上做优化:设计合理的键值与值的长度
- 选择合适的持久化方式 4.0 后:选择混合持久化;只是做缓存,不需要设置持久化
5.1 设置键值的过期时间
我们应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会帮你自动清除过期的键值对,以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略。
Redis 有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除) :
- EXPlRE 命令用于将键key 的生存时间设置为ttl 秒。
- PEXPIRE 命令用于将键key 的生存时间设置为ttl 毫秒。
- EXPIREAT < timestamp> 命令用于将键key 的过期时间设置为timestamp所指定的秒数时间戳。
- PEXPIREAT < timestamp > 命令用于将键key 的过期时间设置为timestamp所指定的毫秒数时间戳。
5.2 使用 lazy free 特性(惰性删除)
lazy free 特性是 Redis 4.0 新增的一个非常使用的功能,它可以理解为惰性删除或延迟删除。意思是在删除的时候提供异步延时释放键值的功能,把键值释放操作放 IO(Background I/O) 单独的子线程处理中,以减少删除删除对 Redis 主线程的阻塞,可以有效地避免删除 big key 时带来的性能和可用性问题。
lazy free 对应了 4 种场景,默认都是关闭的:
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
- lazyfree-lazy-eviction:表示当 Redis 运行内存超过最大内存时,是否开启 lazy free 机制删除;
- lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除;
- lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除;
- slave-lazy-flush:针对 slave(从节点) 进行全量数据同步,slave 在加载 master 的 RDB 文
件前,会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。
建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del
等配置,这样就可以有效的提高主线程的执行效率。
5.3 限制 Redis 内存大小,设置内存淘汰策略
redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
最佳设置: 是物理内存的75% ,写操作比较多 60%
# 最大缓存
maxmemory 1048576
maxmemory 1048576B
maxmemory 1000KB
maxmemory 100MB
maxmemory 1GB
Redis缓存淘汰策略
redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
maxmemory-policy voltile-lru,支持热配置 内存淘汰策略在 Redis 4.0 之后有 8 种:
- noeviction :不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
- allkeys-lru :淘汰整个键值中最久未使用的键值;
- allkeys-random :随机淘汰任意键值;
- volatile-lru :淘汰所有设置了过期时间的键值中最久未使用的键值;
- volatile-random :随机淘汰设置了过期时间的任意键值;
- volatile-ttl :优先淘汰更早过期的键值。
在 Redis 4.0 版本中又新增了 2 种淘汰策略:
- volatile-lfu :淘汰所有设置了过期时间的键值中,最少使用的键值;
- allkeys-lfu :淘汰整个键值中最少使用的键值。
其中 allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数据。
我们可以根据实际的业务情况进行设置,默认的淘汰策略不淘汰任何数据,在新增时会报错。
5.4 禁用长耗时的查询命令
Redis 绝大多数读写命令的时间复杂度都在 O(1) 到 O(N) 之间,在官方文档对每命令都有时间复杂度说明
其中 O(1) 表示可以安全使用的,而 O(N) 就应该当心了,N 表示不确定,数据越大查询的速度可能会越慢。因为 Redis 只用一个线程来做数据查询,如果这些指令耗时很长,就会阻塞 Redis,造成大量延时。
要避免 O(N) 命令对 Redis 造成的影响,可以从以下几个方面入手改造:
- 禁止使用 keys 命令;
- 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
- 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
- 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
- 删除 (del) 一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink ,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
1 Redis 为何选择单线程模型
可维护性高
多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题。单线程模式下,可以方便地进行调试和测试。
IO多路复用
一旦受到网络请求就会在内存中快速处理,由于绝大多数的操作都是纯内存的,所以处理的速度会非常地快。也就是说在单线程模式下,即使连接的网络处理很多,因为有IO多路复用,依然可以在高速的内存处理中得到忽略。
单线程的状态效率能满足
读请求11W+ 写请求8.7W+。
多线程能够充分利用CPU的资源,但对于Redis来说,由于基于内存速度那是相当的高,能达到在一秒内处理10万个用户请求,如果一秒十万还不能满足,那我们就可以使用Redis分片的技术来交给不同的Redis服务器。这样的做法避免了在同一个 Redis 服务中引入大量的多线程操作。
总结:基于内存而且使用多路复用技术,单线程速度很快,又保证了多线程的特点。因为没有必要使用多线程。
FD是一个文件描述符,意思是表示当前文件处于可读、可写还是异常状态。使用 I/O 多路复用机制同时监听多个文件描述符的可读和可写状态。 你可以理解为具有了多线程的特点。
Redis 在 6.0 之后加入了多线程
- 因为读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间,如果把网络读写做成多线程的方式对性能会有很大提升。
- Redis可以使用del命令删除一个元素,如果这个元素非常大,可能占据了几十兆或者是几百兆,那么在短时间内是不能完成的,这样一来就需要多线程的异步支持。
总结:
- Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;
- 而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率
5.5 使用 slowlog 优化耗时命令
我们可以使用 slowlog 功能找出最耗时的 Redis 命令进行相关的优化,以提升 Redis 的运行
速度,慢查询有两个重要的配置项:
- slowlog-log-slower-than :用于设置慢查询的评定时间,也就是说超过此配置项的命令,将会被当成慢操作记录在慢查询日志中,它执行单位是微秒 (1 秒等于 1000000 微秒);
- slowlog-max-len :用来配置慢查询日志的最大记录数。
我们可以根据实际的业务情况进行相应的配置,其中慢日志是按照插入的顺序倒序存入慢查询日志中
我们可以使用 slowlog get n
来获取相关的慢查询日志,再找到这些慢查询对应的业务进行相关的优化。
5.6 避免大量数据同时失效
Redis 过期键值删除使用的是贪心策略,它每秒会进行 10 次过期扫描,此配置可在 redis.conf 进行配置,默认值是 hz 10,Redis 会随机抽取 20 个值,删除这 20 个键中过期的键,如果过期 key 的比例超过 25% ,重复执行此流程。
如果在大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描删除过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程会导致 Redis 的读写出现明显的卡顿,卡顿的。另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU。
5. 7 使用 Pipeline 批量操作数据
Pipeline (管道技术) 是客户端提供的一种批处理技术
可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
5.8 客户端使用优化
在客户端的使用上我们除了要尽量使用 Pipeline 的技术外,还需要注意要尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
5.9 使用分布式架构来增加读写速度
Redis 分布式架构有重要的手段:
- 主从同步
- 哨兵模式
- Redis Cluster 集群
特点说明
- 使用主从同步功能我们可以把写入放到主库上执行,把读功能转移到从服务上,因此就可以在单位时间内处理更多的请求,从而提升的 Redis 整体的运行速度。
- 哨兵模式是对于主从功能的升级,但当主节点奔溃之后,无需人工干预就能自动恢复Redis 的正常使用。
- Redis Cluster 是 Redis 3.0 正式推出的,Redis 集群是通过将数据库分散存储到多个节点上来平衡各个节点的负载压力。 强调一点:cluster 的从机 默认是不读不写,就是备份和容灾
Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,计算公式:slot = CRC16(key) & 16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。这样 Redis 就可以把读写压力从一台服务器,分散给多台服务器了,因此性能会有很大的提升。
- 数据量不多,并发也不多 我们就用单机
- 数据量不是很多,并发很多(超过10万的读)。我们一般是使用主从+sentinel 高可用,写是主库,读是从库,每增加10万并发读 加一个从机
- 数据量大, 我们都是采用cluster 多读写。10个 4G内存 肯定比一个40G 便宜
5.10 禁用 THP 特性
Linux kernel 在 2.6.38 内核增加了 Transparent Huge Pages (THP) 特性 ,支持大内存页 2MB分配,默认开启。
当开启了 THP 时,fork 的速度会变慢,fork 之后每个内存页从原来 4KB 变为 2MB,会大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的 incr 命令也会出现在慢查询中,因此 Redis 建议将此特性进行禁用,禁用方法如下:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
为了使机器重启后 THP 配置依然生效,可以在 /etc/rc.local 中追加 echo never >
/sys/kernel/mm/transparent_hugepage/enabled