一、设计优化
1. 估算Redis内存使用量
以非数字的字符串键值对为例,假设key和value的长度均为12个字节,则内部使用的编码方式为embstr。共计90000个键值对占用的空间
Redis中存储键值对使用字典,字典内部使用哈希表数组,数组的每个元素dictEntry中共有三个指针(指向键的指针,指向值的指针,指向下一个节点的指针),在64位系统中,每个指针占用8字节,则共计24个字节,向上取2的整数幂,则分配32个字节。
一个key,使用SDS存储,数据大小12字节,len+alloc+flags+空字符共4个字节(3.2版本之后),共计12+4=16个字节
一个value,外层使用对象redisObject并指向一个SDS(存放值内容)。对象内存占用16个字节,SDS需要16个字节
综上,一个dictEntry使用的内存总共为 32 + 16 + 16 + 16 = 80字节。
存储90000个键值对需要的bucket数组大小为90000向上取2的整数幂,即131072;每个bucket元素占用8字节(因为内部存储的指针)。
存储90000个键值对占用的总内存:90000*80 + 131072*8 = 82488576。
当存储的键值对长度由12字节增加到13字节,对应的SDS变成17字节,jemalloc分配32个字节,因此每个dictEntry占用的字节数变成112字节。则存储90000个的内存占用变为 90000*12 + 131072*8 = 11128576。
2. 优化内存占用
1. 利用jemalloc特性进行优化
jemalloc是Redis的默认内存分配器,在64位系统中,将内存空间划分成小、大、巨大三个范围;每个范围又划分为许多小的内存块单位,当Redis存储数据时,选择适合的内存块进行存储。譬如存储130字节的对象,jemalloc会将其放入到160字节的内存单元中。
2. 使用整型/长整型
Redis存储字符串的编码类型有三种,当字符串为数字时,使用int(8字节)存储代替字符串,可以节省很多空间。
3. 共享对象
共享对象可以减少对象的创建,包括redisObject的创建。Redis中的共享对象目前只有0-9999,可以通过REDIS_SHARED_INTEGERS参数提高,譬如调整到20000,则0-19999都可以共享
4.缩短键值对的存储长度
大键值对,延长写入和读取耗时、延长持久化需要时间,延长网络传输时间,并且占用内存多,更容易触发内存淘汰机制。尽量缩短存储长度,必要时进行压缩和序列化
二、设置键值的过期时间
Redis的serverCron函数定期清除过期键,节约内存占用,避免键值对过多堆积,频繁触发内存淘汰机制
三、限制Redis内存大小
在64位系统中,默认没有设置最大内存,配置项maxmemory被注释了。当物理内存不足时,使用磁盘作为虚拟内存,将物理内存中的部分数据存放到虚拟内存,这个操作会阻塞Redis进程。当设置了最大内存,当超出限制时,触发内存淘汰。内存淘汰策略在Redis4.0后有8种,主要用到以下原理
- LRU(Least Recently Used,最近最少使用)原理:使用链表保存缓存数据,越靠近表头,存放的数据访问时间越近。当有新数据时插入表头,当有缓存命中时,将数据移至表头,当内存不足时,丢弃表尾数据
缺点是类似全表扫描时,会将链表数据污染
- LFU(Least Frequently Used,最不经常使用策略)原理:记录内存块的使用次数,回收时,按照访问次数排序,当缓存不足时,将使用频率最低的内存释放。
缺点是短时间内大量访问的数据很难删除
1. Redis缓存淘汰策略
- noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-random:随机淘汰任意键值;
- volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值;
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
- allkeys-lfu:淘汰整个键值中最少使用的键值;
四、使用Lazy free特性(Redis4.0新增)
删除大键值对比较耗时,造成主线程的阻塞,为此将删除的操作放在子线程中。共有四项配置:
- lazyfree-lazy-eviction:当Redis运行内存超过最大内存,是否启用lazy free
- lazyfree-lazy-expire:当设置了过期键,在键过期之后,是否启用lazy free
- lazyfree-lazy-server-del:有些命令会隐式删除键,比如rename命令,对这些命令执行时是否启用lazy free
- slave-lazy-flush:从节点加载主节点的RDB文件前,会运行flushall清理原有数据,此时是否启用lazy free
五、禁用长耗时的查询命令
Redis大部分的读写命令的时间复杂度在O(1)到O(N)之间。对于O(N)的命令,需要谨慎使用,如果执行时间过长,将会阻塞Redis
- 禁用Keys
- 避免一次查询所有键,使用scan命令进行分批遍历
- 控制Hash、Set、Sorted Set结构的数据大小
- 将排序、并集、交集放到客户端进行
- 删除大数据,使用unlink,启用新线程删除目标数据(Redis 6.0启用多线程的原因,增加I/O操作并发)
六、使用slowlog优化耗时命令
使用slowlog命令找出高耗时的Redis命令。慢查询的配置项:
- slowlog-log-slower-than:慢查询评定的时间阈值,单位微妙
- slowlog-max-len:配置慢查询日志的最大记录数
七、避免大量数据同时失效
serverCron函数每100毫秒执行一次过期扫描。随机抽取过期键字典中的20个键,删除其中的已经过期的键,判断过期键的比例是否超过25%,重复执行此流程。如果一次扫描中删除了大量过期键,将会造成阻塞。
在设置过期时间时,加入随机数。
八、检查数据持久化策略
Redis4.0之后,加入混合持久化功能,结合了RDB和AOF。在写入时,将当前数据以RDB的形式写入文件的开头,后续的操作命令以AOF的格式存入文件;在加载时,先加载RDB文件,再加载AOF命令。
RDB持久化,可能存在一定时间内的数据丢失。AOF持久化,文件较大时执行较慢,影响启动速度。在非必须持久化操作时,可以关闭持久化,避免间歇性的卡顿(serverCron函数周期性执行持久化操作)
九、使用Pipeline批量操作数据
十、客户端使用优化
使用Redis连接池,减少网络传输次数和非必要调用指令。
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig;
十一、使用分布式架构增加读写速度
Redis分布式架构有:
- 主从同步:读写分离
- 哨兵:自动容灾
- Redis Cluster集群:多读多写,高扩展(集群可添加节点)高可用(某主节点无从节点自动将其他主节点多余的从节点转移),自动容灾
详细:https://www.jianshu.com/p/f0c01c528d8d
十二、其他优化
使用物理机非虚拟机。虚拟机和物理机共享物理网口,并且一台物理机可能有多个虚拟机运行,在内存占用和网络延迟上性能较差
十三、禁用THP特性
Linux默认开启,支持大内存页2MB分配。
开启THP之后,fork速度变慢,fork之后每个内存页从4KB变成2MB,大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询。
Redis雪崩现象:
产生条件:1. 大量缓存同时失效 2.大量并发请求访问失效缓存 导致数据库宕机
解决方案:过期时间设置加入随机数
Redis缓存击穿:
缓存中没有(过期),但数据库中存在数据。此时大量并发请求访问这部分数据,数据库压力陡增。缓存雪崩是大量的缓存击穿。
解决方案:
1.设置热点数据永不过期
2.接口限流熔断和降级
3.布隆过滤器。bloomfilter类似一个哈希表但是不存key,快速判断一个元素是否在集合中。使用多个哈希函数计算入参key得到坐标,在一个bit数组中存储对应位置为1。查找时只要有一个位置为0则不存在,但都为1也有可能不存在。使用多个哈希函数是基于哈希冲突的考量。
4.加锁。这种情况,只允许能有一个线程查询数据库,获取结果后将结果放入缓存,其余线程则直接访问缓存
public String getData(String key) throws Exception{ String data = redis.get(key); if(data == null){ if(lock.tryLock()){ data = redis.get(key); if(data == null){ //查询数据库 data = mysql.select(); redis.set(key,data); }else{ return data; } }else{ Thread.sleep(1000); return getData(key); } } return data; }
Redis缓存穿透:
缓存和数据库中都没有某数据,但有大量的并发请求查询这些数据。导致数据库宕机,主要考虑是漏洞攻击
解决方案:
1. 接口层加校验,譬如用户鉴权
2. 将对应的key的value设置为null存入缓存