1、概述
Redis,全称Remote Dictionary Server,远程字段服务。一个开源的KV键值对数据库。
数据结构复杂
Redis相比于传统的KV键值对数据库,能够支撑更复杂的数据类型。这意味着Redis除了缓存还可以承载更复杂的业务场景,并且还在不断发展更多的业务场景。
持久化
Redis除了数据全部保存在内存中,读写性能很高之外。数据还被保存在磁盘中,因此数据就有了安全性,也可以被当做数据库来用。
所以官方对Redis给了几个定位:Cache(缓存)、Database(数据库)、Vector Search(向量搜素)
2、Redis线程模型
客户端多线程、服务端单线程。
Redis为了维护与更多的客户端进行连接,还是使用多线维护与客户端的Socket连接。其中redis.conf中有一个maxclients
用于配置支持最大的客户端连接数。
但是为了响应读写键值对的网络IO请求,则是单独由一个主线程完成。是基于epoll
实现了IO多路复用,这样就可以一个主线程来处理多个客户端的IO请求。
这里需要注意,主线程并发收到多个请求后,在Redis后端都会转为串行的方式执行。这也是Redis性能很高的原因,不用处理类似MySQL的脏读、幻读、不可重复读之类的问题,单线程也不用发生线程上下文切换造成额外开销。 – Redis当前的性能瓶颈是在内存和网络上面
补充一点,在Redis6.x、7.x版本,为了充分发挥多核CPU的性能,将一些非主核心功能改为了多线程
。例如持久化RDB、AOF、集群数据同步等比较耗时的操作,这样也不会阻塞主线程,也能充分发挥cpu优势。例如flushall 就提供了异步的方式:flushall async、unlink
3、指令原子性
Redis的服务端虽然是单线程处理键值的读写请求,但是在多客户端并发的情况下,会有指令中间穿插的情况。
3.1、复合指令
Redis提供了一些复合指令,也就是一个指令干了多个普通指令的功能,以指令为单位保持原子性。例如mset、getset、setex、setnx。
3.2、事务
像MySQL一样,Redis也提供了事务。
局限:并不能像MySQL那样,当某个操作失败时整体可以回滚。Redis的事务中某个操作失败时,其它操作的执行依旧执行,不会阻断,不会回滚。
也就是Redis事务仅能保证原子性,即事务中的一串操作,不被其它客户端插队
127.0.0.1:6379> MULTI -- 开启事务
OK
127.0.0.1:6379(TX)> set k2 2
QUEUED
127.0.0.1:6379(TX)> incr k2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> lpop k2
QUEUED
127.0.0.1:6379(TX)> incr k2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED --- 也就是事务内所有的操作都排队放进了一个队列,即不被加塞
127.0.0.1:6379(TX)> exec -- 执行事务
1) OK
2) (integer) 3
3) "3"
4) (error) WRONGTYPE Operation against a key holding the wrong kind -- 有一个执行失败了,但是整体并没有受到影响
of value
5) (integer) 4
6) "4"
Redis事务还提供了一个watch功能
,可以在监听某个key,如果在事务执行前,对应key被改了,就会导致操作该key失败。 – 仅对开启该watch功能客户端生效,不影响其它客户端。
Redis事务回滚的仅是操作,不是数据。 如果是编译错误,则不会执行所有操作。 如果非编译错误,则不影响其它操作执行。
客户端只要提交exec了事务,就算客户端立马断开,事务也会继续执行。
3.3、Pipeline
使用管道关键字pipe
可以一次性向Redis提交一组命令,这样做的好处是可以减少网络开销,即减少RTT
。
RTT:客户端通过网络向Redis服务器发送读写请求,然后客户端通过网络接收到Redis服务的响应,这个过程花费的时间叫RTT。
注意事项
1、这一组命令可能会被其它客户端的指令加塞执行,因此并不是原子性的。因此这里可以得出复合指令和事务会阻塞其它客户端的命令执行,但是管道不会。
2、如果一次管道的命令过多时,Redis会耗费额外的内存,因为会缓存每个指令的执行结果,在没执行完毕前,都需要耗费额外的内存来存储。
3、pipeline没执行完毕前,客户端也会阻塞。
4、因此pipeline适合在非热点时间段执行数据调整类的任务。
3.4、Lua
lua是一种小巧的单线程
模型脚本语言,同时拥有一些高级语言特性,比如参数类型、作用域、函数等。因此该脚本语言天然原子性。
简单示例
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2
key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
在lua脚本中可以使用redis.call()
来调用Redis的指令:
127.0.0.1:6379> set stock_1 1
OK
--1 1010
127.0.0.1:6379> eval "local initcount = redis.call('get', KEYS[1]) local a = tonumber(initcount) local b = tonumber(ARGV[1]) if a >= b then redis.call('set', KEYS[1], a) return 1 end redis.call('set', KEYS[1], b) return 0 " 1 "stock_1" 10
(integer) 0
127.0.0.1:6379> get stock_1
"10"
注意事项
1、不会被其他客户端加塞指令,同时相比上面的几种,lua能实现更多的复杂逻辑,因此被大量使用。
2、在lua脚本中不能出现死循环与耗时操作,因为会阻塞其它指令的执行(当然这里可以对lua脚本设置超时时间默认5秒
,超时后,其它指令会直接返回busy阻断),而pipeline就不会阻塞。
3、lua脚本可以设置只读
标识,如果脚本中发现有修改操作就会阻断,通过eval_ro
指令触发。 这样做的好处是可以随时script kill
, 也可以限制用户的修改操作、也可以转移到redis的从读节点执行,减轻主节点压力。
4、热点lua脚本,为了减少网络开销,也可以缓存在redis中。
3.5、redis function(7.0+版本才支持)
这个是给lua脚本的加强,在lua脚本中自己每次都要定义实现,但是redis提供了function功能,别人可以向redis注册已经稳定强大的function方法,然后其它开发者在lua脚本中可以直接使用。
简单示例:
注册
#!lua name=mylib
local function my_hset(keys, args)
local hash = keys[1]
local time = redis.call('TIME')[1]
return redis.call('HSET', hash, '_last_modified_', time, unpack(args))
end
redis.register_function('my_hset', my_hset)
使用
127.0.0.1:6379> FCALL my_hset 1 myhash myfield "some value"
another_field "another value"
(integer) 3
127.0.0.1:6379> HGETALL myhash
1) "_last_modified_"
2) "1717748001"
3) "myfield"
4) "some value"
5) "another_field"
6) "another value"
注意事项
1、function同样也可以设置只读。
2、在redis集群中load function的话,需要每个节点都单独load一次。
3、function会缓存在客户端,因此不建议使用太大,太多的。
4、function和script一样,也有对应的管理命令。
4、bigkey排查
可以通过如下命令找到
redis-cli --bigkeys -- 异步的,但是会短暂占用cpu (可以使用指定-i 0.1代表每100次扫描暂停0.1s,本质上是基于scan)
# Scanning the entire keyspace to find biggest keys
Biggest string found so far 'user:1000' with 1024 bytes
Biggest list found so far 'task_queue' with 50000 items
redis-cli --memkeys --samples 100 指定采样数量 - 同步的,开销很大
Sampling 100 keys in the keyspace!
Key 'cache:large_data' consumes 2048 MB
Key 'logs:20230906' consumes 1536 MB
注意: 在业务低峰执行,不然会影响主线程处理其它客户端请求。另外尽可能的在读从节点执行。
5、数据安全性
Redis之所以性能高得益于内存高效的读写性能,但是内存中的数据断电即丢失,因此在实际项目中一旦需要保存数据,就不能仅使用内存。为此在实际项目中需要在读写性能与数据安全性之间找到一个平衡点。
5.1、Redis持久化机制
1、无持久化:完全关闭数据持久化功能,Redis仅当做缓存来用。
2、RDB(Redis Database):按照一定时间间隔缓存Redis所有数据
快照。
3、AOF(Append Only File):记录Redis的每一次写操作
,当进程崩溃后就可以通过重放的方式恢复数据。
4、RDB + AOF:同时保留Redis的数据和操作。
5.1.1、2种方式的优缺点
RDB
优点
1、RDB文件紧凑(文件小),非常适合定期备份数据。
2、RDB非常适合容灾恢复。(恢复快)
3、RDB备份时,是采用fork一个子进程的方式进行备份,不会影响主进程核心读写功能。(仅是fork时会短暂阻塞一下)
4、与AOF相比,重启恢复会快很多。
5、有redis-check-rdb
工具修复rdb文件
缺点
1、RDB是按照一定间隔备份,这间隔期间的数据容易丢失。
2、RDB在备份过程中会fork一个子进程,将内存复制一份。如果数据量太大或者CPU不是太好会造成Redis短暂的阻塞。相比之下,AOF属于细水长流,每次仅记录新增的写操作并且还可以调整重写
频率。
AOF
优点
1、AOF持久化数据更安全,比如默认是每秒进行一次AOF文件写入,这样即使服务器崩溃,最多丢失1秒的数据。
2、AOF持久化的方式是每次增量的追加写操作,不会出现数据不完整的情况。即使某些极端情况下产生了少数的不完整数据,也可以通过redis-check-aof
工具快速恢复。
3、当AOF文件太大时,会自动新启一个AOF文件写入,不会有一个文件太大的情况。
4、AOF记录的日志较简单,例如当误执行了一条flushall后,可以修改aof文件,将最后一行flushall指令去掉,进行恢复。
缺点
1、针对同样的数据集,AOF文件比RDB大很多。
2、在写操作频繁的情况下,AOF备份的性能比RDB差。
5.1.2、使用建议
1、如果仅是当缓存用,完全可以关闭持久化机制。
2、如果较关注数据的安全性,并且允许丢失一小段时间的数据,可以使用RDB,这样性能是比较高的。
3、不建议单独使用AOF,使用RDB + AOF可以使恢复速度更快,占用的磁盘也更小点。
5.2、RDB详解
指定时间间隔,备份当前时间点内存中的全量数据到磁盘文件(dump.rdb)。 恢复数据是将RDB文件直接丢在内存中。
因为rdb文件是全量数据,因此可以很好的用来同步数据,仅需要将最近的rdb文件同步到另一个redis。
5.2.1、关键配置
1、备份频率配置
# Unless specified otherwise, by default Redis will save the DB:
# * After 3600 seconds (an hour) if at least 1 change was performed
# * After 300 seconds (5 minutes) if at least 100 changes were performed
# * After 60 seconds if at least 10000 changes were performed
#
# You can set these explicitly by uncommenting the following line.
#
# save 3600 1 300 100 60 10000
2、dir文件目录
3、dbfilename文件名默认为dump.rdb
4、rdbcompression是否启动rdb压缩,默认是yes,如果愿意用磁盘换性能,可以关掉。
5、stop-writes-oin-bgsave-error默认yes,在bgsave写入rdb文件失败时,Redis拒绝客户端的写入请求。如果可以接受数据不一致,或者有其它办法保证数据一致性和安全,可以设置no。
6、rdbchecksum默认yes。在存储快照成功后,会使用crc16算法校验数据,这样做有10%的性能损耗。如果希望性能极大的提升,可以关闭此项。
5.2.2、触发备份时机
1、配置的频率。
2、手动执行save或bgsave。bgsave相比较save不会阻塞主进程,但是会fork一个子进程,同时复制一份内存,会消耗更多的内存和CPU。
3、主从复制时,会触发RDB备份。
lastsave
指令可以查看最近一次备份成功的时间。记录的内容是一个毫秒为单位的时间戳。
5.3、AOF详解
按照追加的形式仅记录写操作,不允许改写aof文件。
5.3.1、相关重要配置
1、appendaof 是否开启AOF配置,默认不开启。
2、appendfilname aof文件名称。
# - appendonly.aof.1.base.rdb as a base file.
# - appendonly.aof.1.incr.aof, appendonly.aof.2.incr.aof as incremental files.
# - appendonly.aof.manifest as a manifest file.
appendfilename "appendonly.aof"
redis6.x版本的文件结构:一个文件,包含实际RDB二进制部分和文本的AOF部分。
redis7.x版本的文件结构:三个文件,base.rdb、incr.aof、manifest
3、appendfsync落盘方式。默认everyone,每秒落盘一次。no,靠操作系统自己fsync落盘。 always,每次写操作时自动落盘。
4、appenddiename,设置aof文件目录。
5、auto-aof-rewrite-percentage,auto-aof-rewrite-min-size文件重写触发策略。默认每个文件64MB,写到100%,进行一次重写(例如将多个incr指令合并成一个set,也会新增base.rdb)。另外也可以通过bgrewriteaof
手动触发重写
6、no-appendfsync-on-rewrite aof重写期间是否fsync落盘。
5.4、混合持久化机制
RDB和AOF各有优劣,所以Redis是支持同时开启这2个备份策略的,即混合持久化策略。
# Redis can create append-only base files in either RDB or AOF formats. Using
# the RDB format is always faster and more efficient, and disabling it is only
# supported for backward compatibility purposes.
aof-use-rdb-preamble yes
同时开启这2种方式,Redis在重启恢复数据时还是会选择aof文件,但是还是建议开启rdb备份,因为rdb可以方便定期做数据全量备份(相当于是一个后手),而aof只适合增量备份。
另外该备份方式是单机的,如果机器磁盘坏掉,数据依旧会丢失。
5.5、Redis主从复制Replica机制
也就是主从复制,Redis主节点接受到数据变更时,自动异步将数据同步给多个slave节点。
适用场景:
1、读写分离,master写,slave读。
2、数据备份 + 容灾恢复
如果从节点在配置主节点之前已经有数据了,则会丢弃,直接完全以主节点为主
5.5.1、配置方式
配从不配置主,仅需要配置从节点的主节点源。主节点不需要做任何配置变动。
replicaof host port
slaveof host port
5.5.2、主从状态查看
可以使用info replication
指令查看。
主节点
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.65.214,port=6380,state=online,offset=56,lag=1 # 从节点状态
master_failover_state:no-failover
master_replid:56a1835bdb1f02d2398fac3c34a321e665b07d36
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:56 # 同步偏移量
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:56
从节点
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:192.168.65.214
master_port:6379
master_link_status:up # 主从连接状态
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_read_repl_offset:574
slave_repl_offset:574
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:56a1835bdb1f02d2398fac3c34a321e665b07d36
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:574
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:15
repl_backlog_histlen:560
默认情况下Redis从节点是只读
的,因为被同步数据的从节点也支持写数据的话,会造成数据不一致的情况。
127.0.0.1:6380> set k4 v4
(error) READONLY You can't write against a read only replica.
redis配置中可以修改
# Since Redis 2.6 by default replicas are read-only.
#
# Note: read only replicas are not designed to be exposed to untrusted clients
# on the internet. It's just a protection layer against misuse of the instance.
# Still a read only replica exports by default all the administrative commands
# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve
# security of read only replicas using 'rename-command' to shadow all the
# administrative / dangerous commands.
replica-read-only yes
为了防止危险指令执行,可以使用rename
屏蔽危险指令,例如flushall flushdb keys
6、Redis压测工具
因为Redis通常是挡在流量冲击的第一线,因此对Redis能做多少并发需要特别注意。 Redis提供了一个压测工具用于压测redis-benchmark
。
# 20 100W redis set ( ) redis-benchmark -a 123qweasd -t set | get -n 1000000 -c 20
... Summary:
throughput summary: 116536.53 requests per second
latency summary (msec):
avg min p50 p95 p99
0.111 0.032 0.111 0.167 0.215 3.199