一、设计优化

  1. 估算Redis内存使用量

  以非数字的字符串键值对为例,假设key和value的长度均为12个字节,则内部使用的编码方式为embstr。共计90000个键值对占用的空间

Redis性能调优

  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特性进行优化

    Redis性能调优

    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缓存淘汰策略

    1. noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
    2. allkeys-lru:淘汰整个键值中最久未使用的键值;
    3.  allkeys-random:随机淘汰任意键值;
    4. volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
    5. volatile-random:随机淘汰设置了过期时间的任意键值;
    6. volatile-ttl:优先淘汰更早过期的键值;
    7. volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
    8. 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性能调优

 

十、客户端使用优化

  使用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存入缓存