redis-单机场景
我把单机和集群分开总结,这篇主要是单机下的基础,优化方案等
概要
有些细节直接看书就行,就不再重复了,一些常用的命令也不说了。重点说使用场景,注意事项,性能分析调优等。 先简单介绍下最基本的数据结构:
|
redis存储的数据都是以上述redisObject
对象方式存储,主要是为了几个功能: 节省内存、增加内存回收机制、对外的api掩盖了编码不同的复杂
基础命令
编码方式在什么情况下转换细节不说了,具体可以看《redis设计与实现》
string
- 字符串类型数据会有3种编码方式: int、embstr、raw;
- 使用场景:
- 最常用的缓存功能: key是关键字,value是缓存信息
- 限速: 比如验证码每隔5秒才能重新请求一次,设定一个key超时时间,获取不到key时才能重新请求验证码
hset
- 字典类型有2种编码方式: ziplist、hashtable
- 使用场景:
- 整合string: 如果同类数据都单独kv存储,键过多浪费内存,在业务上也不直观,这时候用hset内聚
- 字符串序列化: 直接将序列化数据一键保存,坏处是数据量大的情况下要全部取出,修改后再更新,并且序列化有一定的开销。如果用字典取代则第一次存储会麻烦一点,但之后可以指定具体key进行修改,不过hset的整体内存消耗也会大于一个简单的k/v。
list
列表结构对插入,查询是有顺序的
- 列表类型有2种编码方式: ziplist、quicklist、linkedlist
- 使用场景:
- 队列: lpush + rpop
- 栈: lpush + lpop
- 消息队列: 命令增加
b
则可以阻塞,用lpush+brpop 就可以当成一个简单的消息队列使用
set
set不允许数据重复,set之间可以进行交并差集操作
- 列表类型有2种编码方式: intset(所有元素都是整数)、hashtable
- 使用场景:
- 标签: 每个用户有一个爱好集合,多个用户直接查看共同爱好只需要进行交集操作
sinter user1 user2
- 抽奖: 生成n个随机数写入set,然后每个用户都能进行
spop key
获得一个数字
- 标签: 每个用户有一个爱好集合,多个用户直接查看共同爱好只需要进行交集操作
zset
有序集合根据分值进行排序,增加一个元素都会设置一个分值,之所以用skiplist好处是范围查询。
- 列表类型有2种编码方式: ziplist、skiplist
- 使用场景:
- 排行榜: 根据游戏玩家战斗积分进行排名
有用的小功能
这里介绍一些不太常用,但比较有用的redis操作
Lua & 事务
一组动作要么全部执行成功,要么全部执行失败,这就是事务。 redis中使用事务很简单:
|
redis不支持事务回滚,如果写错了某个命令, 最终执行成功后,是无法回滚的。lua脚本可以提供更强大的功能。
命令:
|
举例:
|
也可以将脚本加载到redis中,达到复用
|
那么事务能做的,用lua脚本也可以实现,并且lua还能更简单的实现复杂的带业务逻辑的事务比如: 需要对某个有序集合范围内的数据进行分值统一修改,
Bitmap
比如有这样一个场景: 游戏服务搞活动,连续7天登录可以获得奖励。 如果用redis实现,那我们要分别记录7条用户id的集合,最后做交集,计算出符合要求的用户,进行奖励发放。 如果用户量很大,每天上线的用户很多,每个集合会很大,这时候用bitmap可以很方便解决。
一个位只能表示0或1,一个byte则能表示8个元素的状态,如果有8个id为1,2,3,4,5,6,7,8的用户,每个用户登录则修改所占位为1,假设第一天前四个用户登录了,那么一个byte则为11110000
,这就是bitmap的原理。随着用户的数量增大,就比其他数据结构节约更大内存。
Hyperloglog
hyperloglog使用基数估算
算法,可以大幅度降低内存占用,用来估算一批数据中不重复的元素数量,比hashmap
、bitmap
内存占用还要小。
但不精准,redis的失误率是0.81%
,这种使用场景主要是数据量大或对精确度不敏感的场景,比如查看google.com
每天的访问总量,这种上亿访问量的站点,即便有几十万,几百万的不精准也不重要。
常用命令:
- PFADD key element [element …] : 添加指定元素到 HyperLogLog 中。
- PFCOUNT key [key …] : 返回给定 HyperLogLog 的基数估算值。
- PFMERGE destkey sourcekey [sourcekey …] : 将多个 HyperLogLog 合并为一个 HyperLogLog
GEO
录入地址的经纬度,就可以获取两个地址的距离,也可以根据经纬度判断是否在某个地址范围
Pipeline 减少网络rtt
多个命令一起发送给redis服务器,redis将结果集统一返回给客户端,减少了多个单一命令的往返时间,多次往返时间变成了一次。 所以在网络延迟大的情况下,pipeline效果更明显。需要注意的是pipeline不是原子操作。
其他命令
- append
|
若有内容则追加,否则和set相同,这个命令的好处是可以追加,也就是无需get后再重新set。
需要注意的是无论原本的编码格式是什么 embstr
或 int
,无论追加什么内容都会改变为 raw
编码
|
- getset 返回老值并赋予一个新值,当没有此值的时候返回空
|
可以理解为 get + set 命令集,且是原子性的
- stream 主要用于消息队列,缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
持久化
redis是内存数据库,内存在程序退出或异常退出后都会丢失,redis提供了两种持久化的方法
rdb
rdb触发条件有两种
-
手动: 手动触发比较简单:使用
save
命令,会阻塞主线程,在完成之前无法使用其他命令,bgsave
会fork子进程异步执行save。 -
自动: 一般配置文件里会有
save m n
相关配置,表示m秒内数据进行n次修改时,自动触发bgsave。
rdb是全量复制,好处是备份方便,备份文件拷贝到其他机器做灾难恢复,也可以对备份后的rdb做分析检查redis内存性能问题。坏处就是需要fork子进程,频繁操作成本高。 无法实时的持久化。
aof
aof提供了命令级别的持久化,会把写入相关命令同步到aof文件中,因为一条写入命令就立刻落地到文件中,会影响redis的高性能。所以官方提供了三种持久化策略进行选择:
-
AOF_FSYNC_NO : redis不做任何同步操作,保存时机由系统决定;因为调用的系统命令
sync
-
AOF_FSYNC_EVERYSEC :每一秒钟保存一次
write会写入命令,但fsync命令才会最终执行同步到文件中,所以redis有一个单独的线程来每秒进行fsync操作,理论上只有在系统突然宕机的情况下丢失1秒的数据。(严格 来说最多丢失1秒数据是不准确的)
- AOF_FSYNC_ALWAYS :每执行一个命令保存一次
每次有写入命令,都会进行同步到aof文件操作,虽然保证了实时性(最多丢失一个命令),但如果同步过程阻塞,则会影响整个redis的命令读写效率。
当然,也提供了手动执行的方式: 命令为 bgrewriteaof
aof是追加式的写入,这样的缺点就是会有重复,比如:
|
两条命令,其实可以合并为一条set a world
,这以点redis提供了重写
机制,用来将已有的aof文件进行整合缩减。
重写条件在配置文件中,比如当aof文件增长比达到 n% 后就会进行重写,会后台启动一个子线程进行重写操作。
内存回收
redis主要有两种回收策略,设置了过期的key,过期后回收。内存达到上限后有限制的回收。这些都有配置进行策略性的回收
过期策略
redis使用了两种方式进行过期键的回收
- 惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存
- 定期过期
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
两者结合来提高内存的释放效率,节省cpu资源
lazy free
若删除一个大的key,因为慢而阻塞redis,所以4.0开始加入了惰性删除。使用命令unlink key
可以主动的使用惰性删除某个key。或根据下面配置,进行被动的删除。
|
无论是主动还是被动他们的流程都是一样的:
- 删除的时候计算Lazy Free方式释放对象的成本,只有超过特定阈值,才会采用Lazy Free方式
- Lazy Free方式会调用bioCreateBackgroundJob函数来使用BIO线程后台异步释放对象。
- 当Redis对象执行UNLINK操作后,对应的KEY会被立即删除,不会被后续命令访问到,对应的VALUE采用异步方式来清理。
若对过期不敏感,可以考虑多个key分散过期时间,防止key都在一个时间内过期造成性能影响。
数据删除
redis作为缓存服务时可以利用下面策略来降低内存, 具体策略受maxmemory-policy参数控制,Redis支持6种策略
- noeviction:不删除任何key,,新写入操作会报错。
- allkeys-lru:在键空间中,移除最近最少使用的key。
- allkeys-random:在键空间中,随机移除某个key。
- volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。
内存优化
基础对象的编码调整
不同的数据有不同的编码方式(encoding)主要是用来节省内存空间,比如使用hset数据类型时,在元素数量小于配置值的时候,同时所有值都小于hash-max-ziplist-value配置时,使用ziplist结构当字典使用,更加节省内存。当超出条件后检索效率会降低,所以会改为hashtable。
每种redis数据结构都有2种或以上的编码方式来实现效率和空间的平衡,元素数量极少的时候即便是0(n²)也可以满足性能需求
对象共享
相同数据多个key会指向同一个对象,这时refcount
会增加引用数量,不过这种节省内存的方式redis只提供了数字上的对象复用,因为判断数字是否一致时间复杂度为0(1),而字符串需要0(n),其他类型可能更复杂,如果复用就导致了时间换空间,对于高性能的redis并不合适。一个数字类型占用空间很小,比一个redisObject
对象小的多,再加上判断快,所以对于数字对象的内存共享是很有意义的。
redis 只支持10000以内的数字对象复用,并且不可配置写死在代码中的。
|
需要注意的是如果开启 maxmemory
和LRU淘汰策略
后对象池就无效了。因为共享一个redisObject后也会共享lru字段:
|
导致无法对每个对象的最后访问时间进行分别记录。
内存碎片
什么是内存碎片?网上看到一个例子非常贴切: 坐高铁,假设一个车厢60个位置,目前空位有3个,但这三个都是独立的位置,不是连续的。这时候如果有3个朋友想坐在一起,就无法满足,只能选其他车厢,也就是整体内存是足够的,但无法提供服务
造成内存碎片主要原因是:
- 为了方便的做内存管理,内存分配器不会完全按照申请的大小做分配,比如
jemalloc
分配器,我们申请15字节,jemalloc
会给我们20字节内存,这样好处是如果继续写入5字节内容,就减少了一次分配次数,但多出来的5字节就是碎片。 - 正常的业务都会对kv内容进行修改删除造成内存扩大或释放
如何判断内存碎片情况:
使用命令:info memory
|
解决方法:
- 重启redis: 重启后数据重新加载,之前非连续的内存就能连续了
- 配置设置: 此配置可以控制redis自动清理
|
一些性能问题
慢查询日志
slowlog-log-slower-than 10000 记录超过10000微妙的命令
slowlog-max-len 128 最多存储128条慢查询日志
使用命令 slowlog get
就能查询出所有符合上述条件的命令
1) 1) (integer) 10466 日志id
2) (integer) 1650529643 命令执行时间
3) (integer) 377462 执行耗时(微妙)
命令和命令参数
4) 1) "LRANGE"
2) "1611a5de-c05f-11ec-9c1a-0050569ae574_20220421_160652_421424"
3) "0"
4) "-1"
5) "127.0.0.1:45984" 执行命令的客户端
6) ""
.....
bigkey
利用命令redis-cli -h ip -p port -a pwd --bigkeys
可以查看bigkeys信息。
redis的命令读写是单线程的,操作大的key会直接影响整个阻塞整个服务。redis4.0 前 删除bigkey会阻塞住,4.0之后支持了异步删除。建议不用redis存,或者将bigkey进行拆分
redis与系统
redis与cpu
- 先简单说说cpu
- cpu: 中央处理器,一个cpu不等于物理核,也不等于逻辑核。
- 物理核: cpu真正的运行单元,有独立的运作能力(能独自运行指令、有独立缓存)
- 逻辑核: 物理核中逻辑层面的核,一个物理核可以有多个逻辑核,物理核通过高速运算,让应用层误以为有多个cpu在运算
奔腾处理器时代,计算机想要提高运算性能,可以使用多个cpu,插入到主板上。但主板上的多个cpu之间进行通信效率非常低,因为通过系统总线完成,所以无法做到1+1=2的效果。既然多个cpu之间通信效率低,于是又在单cpu上进行了研究,之后英特尔开发了超线程(Hyper-threading)
技术,它可以复制cpu内部组件,便于线程之间共享信息,这样的好处是加快了多个计算过程,更高效的利用cpu。假设只有一个物理核的cpu,利用超线程,操作系统误以为有2个物理核,需要注意的是这是提高了cpu的利用率,但并没有真正达到2倍的cpu处理能力。超线程提高了性能, 但并没有达到真正意义上的并行处理,之后多核架构的出现,一个cpu内有多个物理核心,达到了真正意义上的并行处理,多个物理核直接不在靠系统总线传输,而是通过共享芯片的内部总线。
最后多个物理核+超线程,就有了现在的 双核4线程/八核16线程的cpu。
- 对于cpu调用程序
- 软亲和性:进程要在指定的 CPU 上尽量长时间地运行而不被迁移到其他CPU。Linux 内核进程调度器天生就具有被称为 软 CPU 亲和性(affinity) 的特性,因此linux通过这种软的亲和性试图使某进程尽可能在同一个CPU上运行
- 硬亲和性:将进程或者线程绑定到某一个指定的cpu核运行,虽然Linux尽力通过一种软的亲和性试图使进程尽量在同一个处理器上运行,但它也允许用户强制指定进程无论如何都必须在指定的处理器上运行。
目前我们的cpu架构是numa架构(非统一内存访问架构(Non-uniform Memory Access,简称NUMA架构),这意味着物理核之间如果处于不同的numa节点,那么内存是分离的,a核心(socket 1)访问b核心(socket 2)内存数据是需要经过总线的,会增加延迟。
linux使用lscpu
看cpu情况:
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 1
Core(s) per socket: 1
座: 8
NUMA 节点: 1
厂商 ID: GenuineIntel
CPU 系列: 6
型号: 79
型号名称: Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz
步进: 1
CPU MHz: 2199.998
BogoMIPS: 4399.99
超管理器厂商: VMware
虚拟化类型: 完全
L1d 缓存: 32K
L1i 缓存: 32K
L2 缓存: 256K
L3 缓存: 25600K
NUMA 节点0 CPU: 0-7
3种方式
- 指定某个进程绑定到cpu: taskset -pc cpuid 进程id
- 启动进程的时候进行绑定: taskset -c cpuid 程序启动项
- 使用系统调用:
|
redis的持久化,还有个别命令都是在子进程或子线程执行的,也就是说对于redis绑定一个物理核还是有可能阻塞的。另外redis网络用的是io多路复用,监听的是io事件,在使用numa架构的时候我们应该防止redis在绑定cpu时跨节点。
redis 与linux hugepage
- 写时复制
fork是系统命令,主进程执行后,会将内存数据完全的拷贝在子进程中,相当于创建了一个快照。redis用fork的好处是方便,且不影响主进程工作,因为是完全拷贝了主进程的内存,但当redis内存数据非常大的时候,fork会非常慢,若使用了10g内存,fork之后总体就占了20G内存
linux提供了fork的写时复制(copy-on-write),主要作用就是将拷贝推迟到写操作真正发生时,这也就避免了大量无意义的拷贝操作。
也就是在redis进行 rdb save的时候,fork是很快的,因为fork只是子进程指向了与主进程同样的物理内存中,并没有发生内存复制操作。类似应用代码中创建了个指针,两个指针指向的是同一个数据。只有当主进程的数据做了修改,才会开始复制,并且只会复制修改所在内存页的数据,也就是复制的效率取决于redis在save过程中的写命令是否频繁,内存页的大小。但因为redis总体是读多写少。也就是说假设在fork后进行备份过程中,redis并没有任何写入行为,那么fork子进程进行持久化操作是不会产生额外的使用内存。
刚才说内存页也影响了rdb效率,是因为linux hugepage(大内存页),hugepage可以增加命中率减少页数量的,这对数据库来讲是个好处。同样的内存需求情况下内存页大了意味着页表项的减少,这样就可以提高快表的命中率了,linux系统是支持内存大页机制的 默认是2mb: grep Huge /proc/meminfo
,但对于redis进行rdb时利用写时复制,内存页大导致主进程写入操作会复制更大的内存空间和数据,所以如果开启了hugepage redis会有下面log:
|
如果有rdb需求则可以考虑关闭linux hugepage。
redis 与 vm
redis3.0 之前自己开发了vm,之后就去掉了,主要原因可能是代码复杂,重启慢等。3.0开始使用了/proc/sys/vm/overcommit_memory
系统相关的vm,若没设置,会有下面的log
|
linux系统配置/proc/sys/vm/overcommit_memory
有三种策略:
|
配置
|
- 6379来源: 是手机按键的 MERZ,原因是redis作者Antirez在看一个广告,意大利广告女郎「Alessia Merz」在电视节目上说了一堆愚蠢的话。
参考
- 《redis设计与实现》
- 《redis开发与运维》
- http://cenalulu.github.io/linux/numa/
- https://cloud.tencent.com/developer/article/1465603
Gitalking ...