Redis使用技巧及最佳实践

Redis最佳实践

Redis键值设计

优雅的key结构

定义Key时, 最好遵循以下约定:

  • 遵守基本格式: [业务名称]:[数据名]:[数据id]

    login:user:10

    优点在于可读性强, 避免key冲突, 方便管理

  • 长度不超过44字节

    节省内存:

    key是string类型, 底层编码包括int、embstr和raw三种. 
    embstr在小于44字节使用, 采用连续内存空间, 内存占用更少
    

    image-20220701135942117

    image-20220701135958999

  • 不使用特殊字符

拒绝BigKey

Redis中单个Key支持512M大小的数据存储, 但BigKey数据对系统性能有很大印象, 应尽量避免

image-20220701143112476

危害:

  • 网络阻塞

    对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢

  • 数据倾斜

    BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡

  • Redis操作阻塞

    对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞

  • CPU压力

    对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用

判断BigKey

BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:

  • Key本身的数据量过大:一个String类型的Key,它的值为5 MB。

  • Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。

  • Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB。

一般推荐, 单个key的value小于10KB. 对于集合类型的key,建议元素数量小于1000

发现BigKey

  • redis-cli --bigkeys

    利用redis-cli提供的--bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key

    image-20220701142913901

  • scan扫描

自己编程,利用scan扫描Redis中的所有key,利用strlenhlen等命令判断key的长度(此处不建议使用MEMORY USAGE

image-20220701143155339

  • 第三方工具

    利用第三方工具,如 Redis-Rdb-Tools 分析RDB快照文件,全面分析内存使用情况

  • 网络监控

    自定义工具,监控进出Redis的网络数据,超出预警值时主动告警

    image-20220701142452820

处理BigKey

  • 拆分

  • 删除

    BigKey内存占用较多,即便时删除这样的key也需要耗费很长时间,导致Redis主线程阻塞,引发一系列问题。

    • version 3.0及以前

      如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey

      image-20220701142305612

      image-20220701141940391

    • version 4.0之后

      Redis在4.0后提供了异步删除的命令:unlink

      image-20220701142211509

      image-20220701142137813

恰当的数据类型

1.存储用户信息, 存在三种方式:

  • 方式一:json字符串

    key value
    user:1 {“name”: “Jack”, “age”: 21}

    优点:实现简单粗暴

    缺点:数据耦合,不够灵活

  • 方式二:字段打散

    key value
    user:1:name Jack
    user:1:age 21

    优点:可以灵活访问对象任意字段

    缺点:占用空间大、没办法做统一控制

  • 方式三:hash (适合)

    key field value
    user:1 name Jack
    user:1 age 21

    优点:底层使用ziplist,空间占用小,可以灵活访问对象的任意字段

    缺点:代码相对复杂

2.hash类型的key,其中有100万对field和value,field是自增id,这个key存在什么问题?如何优化?

Key field value
id:0 value0
somekey ….. …..
id:999999 value999999
  • 存在的问题:

    ①hash的entry(field-value对)数量超过500时,会使用哈希表而不是ZipList,内存占用较多。

    ②可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题

  • 处理方案:

    拆分为小的hash,将 id / 100 作为key, 将id % 100 作为field,这样每100个元素为一个Hash

    Key field value
    id:00 value0
    key:0 ….. …..
    id:99 value99
    Key field value
    id:00 value0
    key:1 ….. …..
    id:99 value99

    Key field value
    id:00 value0
    key:9999 ….. …..
    id:99 value99
  • 优点: 减少无用结构数据, 避免BigKey产生

设置合理的超时时间

批处理优化

对于大量数据的处理, 存在多次少量和少次多量两个方案, 由于网络传输耗时数量级高于命令执行耗时, 所以优先采用一次性执行多条命令的方案

Redis命令

Redis提供了执行多个数据的命令, 批量查询/插入/修改, 如mset , hmset等, 可以保证原子性

但需要注意, 不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞

Pipeline

MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline功能. 但不保证多条命令的原子性

1
2
3
4
5
$res = $redis->pipeline(function ($pipe) {
$pipe->lPush('list', 'list_item1');
$pipe->llen('list');
$pipe->lRange('list', 0, -1)
})

集群下的批处理

如MSET或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群(Cluster),那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败。

image-20220701155515097

串行命令 串行slot 并行slot hash_tag
实现思路 for循环遍历,依次执行每个命令 在客户端计算每个key的slot,将slot一致分为一组,每组都利用Pipeline批处理。 串行执行各组命令 在客户端计算每个key的slot,将slot一致分为一组,每组都利用Pipeline批处理。 并行执行各组命令 将所有key设置相同的hash_tag,则所有key的slot一定相同
耗时 N次网络耗时 + N次命令耗时 m次网络耗时 + N次命令耗时 m = key的slot个数 1次网络耗时 + N次命令耗时 1次网络耗时 + N次命令耗时
优点 实现简单 耗时较短 耗时非常短 耗时非常短、实现简单
缺点 耗时非常久 实现稍复杂 slot越多,耗时越久 实现复杂 容易出现数据倾斜

hash_tag使用:

image-20220701160111674

对键执行 crc16 算法, 再对 16384 取余,即可得到一个小于 16384 的数,该结果就是 slot编号

服务端优化

持久化配置

Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:

  • 用来做缓存的Redis实例尽量不要开启持久化功能

  • 建议关闭RDB持久化功能,使用AOF持久化

  • 利用脚本定期在slave节点做RDB,实现数据备份

  • 设置合理的rewrite阈值,避免频繁的bgrewrite

  • 配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞, 但需要注意可能造成期间数据丢失问题

部署有关建议:

  • Redis实例的物理机要预留足够内存,应对fork和rewrite

  • 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力

  • 不要与CPU密集型应用部署在一起

  • 不要与高硬盘负载应用一起部署。例如:数据库、消息队列

慢查询

慢查询:在Redis执行时耗时超过某个阈值的命令,称为慢查询。

1
2
3
4
5
慢查询的阈值可以通过配置指定:
lslowlog-log-slower-than:慢查询阈值,单位是微秒(μs)。默认是10000,建议1000

慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:
lslowlog-max-len:慢查询日志(本质是一个队列)的长度。默认是128,建议1000

image-20220701162647571

查看慢查询日志列表:

  • slowlog len:查询慢查询日志长度

  • slowlog get [n]:读取n条慢查询日志

  • slowlog reset:清空慢查询列表

    image-20220701163013117

命令及安全配置

Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网上,而Redis如果没有做身份认证,会出现严重的安全漏洞.

漏洞出现的核心的原因有以下几点:

  • Redis未设置密码

  • 利用了Redis的config set命令动态修改Redis配置

  • 使用了Root账号权限启动Redis

示例

1
2
3
(echo -e "  "; cat pass.txt; echo -e "  ") > pass_e.txt
或者
echo "\r\n\r\n <?php 一句话 \r\n\r\n" > shell.php

image-20220701163543128

建议

为了避免这样的漏洞,这里给出一些建议:

  • Redis一定要设置密码

    image-20220701164358266

  • 禁止线上使用下面命令:keysflushallflushdbconfig set等命令。可以利用rename-command禁用。

    image-20220701164256070

  • bind:限制网卡,禁止外网网卡访问

  • 开启防火墙

  • 不要使用Root账户启动Redis

  • 尽量不使用默认的端口

内存配置

当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。

内存占用来源

内存占用 说明
数据内存 是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题
进程内存 Redis主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略
缓冲区内存 一般包括客户端缓冲区AOF缓冲区复制缓冲区等。客户端缓冲区又包括输入缓冲区输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出

内存碎片: Redis内部内存划分产生, 如10字节的数据在8字节与16字节期间, 故为其分配16字节数据, 多出来的6字节为内存碎片, 可以通过重新启动Redis服务处理

查看内存状态

Redis提供了一些命令,可以查看到Redis目前的内存分配状态:

  • **info memory **

    image-20220701200129343

    image-20220701195822913

  • memory [xxx]

    image-20220701200229761

    image-20220701200205530

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    # memory stats 结果解析
    1) "peak.allocated" // Redis进程启动以来消耗内存的峰值
    2) (integer) 945048
    3) "total.allocated" // Redis使用分配器分配的总字节数, 即当前的总内存使用量
    4) (integer) 885240
    5) "startup.allocated" // Redis启动时消耗的初始内存量
    6) (integer) 801232
    7) "replication.backlog" // 复制积压缓冲区的大小
    8) (integer) 0
    9) "clients.slaves" // 主从复制中其他所有从节点的读写缓冲区大小
    10) (integer) 0
    11) "clients.normal" // 除从节点外, 所有其他普通客户端的读写缓冲区大小
    12) (integer) 41040
    13) "aof.buffer" // AOF持久化使用的缓存和AOF重写时产生的缓存
    14) (integer) 0
    15) "lua.caches" // lua脚本执行缓存
    16) (integer) 0
    17) "db.0" // 当前业务数据库
    18) 1) "overhead.hashtable.main" //当前数据库的hash链表开销内存综合, 即元数据内存
    2) (integer) 152
    3) "overhead.hashtable.expires" // 用于存储可以的过期时间消耗的内存
    4) (integer) 32
    19) "overhead.total" // 总开销
    20) (integer) 842456
    21) "keys.count" // Redis实例中Key的数量
    22) (integer) 3
    23) "keys.bytes-per-key" // Redis实例中每个Key的平均大小
    24) (integer) 28002
    25) "dataset.bytes" // 纯业务数据占用的内存大小
    26) (integer) 42784
    27) "dataset.percentage" // 纯业务数据占用的内存比例
    28) "50.928482055664062"
    29) "peak.percentage" // 当前总内存与历史峰值的比例
    30) "93.671432495117188"
    ...
    49) "fragmentation" // 内存碎片率
    50) "6.1035556793212891"
    51) "fragmentation.bytes"
    52) (integer) 4308544

内存缓冲区配置

内存缓冲区常见的有三种:

  • 复制缓冲区:主从复制的repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过repl-backlog-size来设置,默认1mb

    image-20220701201833474

  • AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行bgrewriteaof的缓冲区。无法设置容量上限

  • 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设

    image-20220701201801038

    image-20220701201920092

    普通客户端无限制缓冲区, 可以通过限制BigKey, 增加服务带宽, 修复对应业务逻辑

定位请求客户端

可以通过以下命令定位缓存溢出问题的客户端

  • info clients

    image-20220701202351824

  • client list

    image-20220701202441793

集群最佳实践

集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:

  • 集群完整性问题

    在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务:

    image-20220701203621904

    为了保证高可用特性,这里建议将 cluster-require-full-coverage配置为no

  • 集群带宽问题

    集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:插槽信息和集群状态信息

    集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。

    解决途径

    ①避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。

    ②避免在单个物理机中运行太多Redis实例

    配置合适的cluster-node-timeout

  • 数据倾斜问题

    出现BigKey或批处理时使用hash_tag , 可能会造成数据集中于某个节点中

  • 客户端性能问题

    客户端在处理集群时, 执行命令前必须要处理插槽的选择等问题, 会造成一定的性能损耗

  • 命令的集群兼容性问题

    批处理命令不能直接向集群中执行, 因为数据可能需要落到不同的slot, 需要客户端自行处理

  • lua和事务问题

    类型与批处理问题

集群还是主从

单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。