简介
redis,Remote Dictionary Server,远程字典服务,一个基于内存的、存储键值对的数据库。redis是开源的,使用C语言编写。因为redis的数据是存储在内存中的,所以redis通常被用来做数据库的缓存
优点:
-
redis是基于内存的,所以它的速度很快,并且支持把内存中的数据持久化到磁盘中,数据可以长期存储,避免丢失
-
redis支持的数据类型比较多
-
redis的所有操作都是原子性的
-
redis支持集群模式
安装
Linux
下载安装包:
-
执行命令:wget http://download.redis.io/releases/redis-6.2.6.tar.gz,可以进入这个网址选择其它版本,命令执行完成之后安装包会被存放到当前目录下。
-
redis 软件是用C语言编写,安装包是源码,需要自己编译
解压安装包:执行命令:tar -zvxf redis-6.2.6.tar.gz
,解压安装包
编译:进入redis安装包的根目录,
-
执行
make MALLOC=libc
命令,它会读取当前目录下的 Makefile 文件,根据文件中的指令来编译 C 语言编写的源码, -
make 命令需要 gcc 编译器的支持。
验证:
-
运行完 make 命令后,C 语言编写的源码就会根据平台生成可执行文件,
-
进入src目录,里面存放的是 redis 软件的源码,
-
此时会发现 ‘redis-cli’、‘redis-server’ 等可执行文件,
-
运行 make test 命令,检测编译后的可执行文件是否是正确的,需要 tcl 工具的支持。
在redis根目录下新建bin目录、conf目录、docs目录
-
将编译完成的可执行命令移动bin目录下
-
将安装包中的文档移动到docs目录下
-
将配置文件移动到conf目录下,包括redis.conf、sentinel.conf
配置环境变量:
-
在/etc/profile文件中配置环境变量,
-
然后执行命令:
. /etc/profile
,使环境变量生效
开启服务器:
-
运行
redis-server
命令,使用默认的配置,启动redis服务 -
redis服务器的默认端口是6379。
-
查看进程信息:
ps -aux | grep redis
进入客户端:运行redis-cli
命令,进入 redis 客户端
关闭客户端:
-
执行quit命令,退出客户端;
-
执行exit命令,退出服务器和客户端
安装redis时需要的软件
wget:
-
安装wget:
yum install -y wget
-
查看wget的安装包:
rpm -qa | grep 'wget'
gcc编译器:
-
安装:
yum -y install gcc gcc-c++
,安装c和c++的编译器 -
查看:
gcc -v
、rpm -q gcc
,执行这两个命令中的任意一个即可
tcl工具:
-
安装:
yum install -y tcl
-
查看:
rqm -q tcl
redis软件包的目录
src/:存放源码
-
redis-server:redis服务器
-
redis-cli:redis客户端,先启动服务器,然后通过客户端连接
redis.conf:redis的核心配置文件
Windows
第一步:网址 https://github.com/microsoftarchive/redis
,在这个网址找Windows的安装包,这是为Windows平台开发redis的项目,
第二步:运行 redis-server.exe,启动redis服务
第三步:运行 redis-cli.exe,启动redis客户端
入门案例
在这里,通过简单地使用redis来了解redis的工作原理
启动redis
方式一:使用默认配置启动redis
执行命令:redis-server
,在这里,执行直接redis-server命令,使用默认的配置来启动redis
[root@Linux4-CentOS7 redis-6.2.6]# redis-server
2862:C 14 Jan 2024 11:40:37.180 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2862:C 14 Jan 2024 11:40:37.180 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=2862, just started
2862:C 14 Jan 2024 11:40:37.180 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
2862:M 14 Jan 2024 11:40:37.181 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2862:M 14 Jan 2024 11:40:37.181 * monotonic clock: POSIX clock_gettime_.__.-``__ ''-.__.-`` `. `_. ''-._ Redis 6.2.6 (00000000/0) 64 bit.-`` .-```. ```\/ _.,_ ''-._( ' , .-` | `, ) Running in standalone mode|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379| `-._ `._ / _.-' | PID: 2862`-._ `-._ `-./ _.-' _.-'|`-._`-._ `-.__.-' _.-'_.-'|| `-._`-._ _.-'_.-' | https://redis.io`-._ `-._`-.__.-'_.-' _.-'|`-._`-._ `-.__.-' _.-'_.-'|| `-._`-._ _.-'_.-' |`-._ `-._`-.__.-'_.-' _.-'`-._ `-.__.-' _.-'`-._ _.-'`-.__.-'2862:M 14 Jan 2024 11:40:37.182 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2862:M 14 Jan 2024 11:40:37.182 # Server initialized
2862:M 14 Jan 2024 11:40:37.182 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
2862:M 14 Jan 2024 11:40:37.182 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').
2862:M 14 Jan 2024 11:40:37.183 * Ready to accept connections
查看日志的最后一行,‘Ready to accept connections’,证明redis服务已经启动。
使用默认配置启动redis服务,redis服务会在前台运行。
查看redis进程:
[root@Linux4-CentOS7 redis-6.2.6]# ps aux | grep redis
root 2862 0.2 0.1 144432 2596 pts/0 Sl+ 11:40 0:01 redis-server *:6379
root 2869 0.0 0.0 112824 988 pts/1 R+ 11:49 0:00 grep --color=auto redis
方式二:使用指定配置文件启动redis
执行命令:redis-server ${REDIS_HOME}/conf/redis.conf
。
打开redis客户端
方式一:使用默认配置
执行命令:redis-cli
,同样使用使用默认配置来连接redis服务器,默认主机地址是当前地址,默认端口是6379
[root@Linux4-CentOS7 redis-6.2.6]# redis-cli
127.0.0.1:6379>
方式二:连接到指定服务器
使用-h参数指定主机地址,使用-p参数指定端口
C:\Users\武耀君>redis-cli -h 192.168.0.6 -p 6379
192.168.0.6:6379>
简单地操作redis
查看当前所有的键:keys *
127.0.0.1:6379> keys *
(empty array)
向redis中存入一个键值对:set aa bb
,在这里,键是aa,值是bb
127.0.0.1:6379> set aa bb
OK
根据键来获取值:get aa
127.0.0.1:6379> get aa
"bb"
查看当前所有的键:
127.0.0.1:6379> keys *
1) "aa"
退出redis客户端
在redis客户端中执行命令:exit
或quit
127.0.0.1:6379> exit
[root@Linux4-CentOS7 redis-6.2.6]#
关闭redis服务
方式一:redis服务在前台运行时
redis服务是在前台运行的,直接按ctrl + c关闭即可
按下ctrl + c之后的部分日志:
^C2862:signal-handler (1705204789) Received SIGINT scheduling shutdown...
2862:M 14 Jan 2024 11:59:49.142 # User requested shutdown...
2862:M 14 Jan 2024 11:59:49.142 * Saving the final RDB snapshot before exiting.
2862:M 14 Jan 2024 11:59:49.147 * DB saved on disk
2862:M 14 Jan 2024 11:59:49.147 # Redis is now ready to exit, bye bye...
方式二:redis服务在后台运行时
redis-cli shutdown
dump.rdb文件
在执行redis-server命令的路径下,可以看到一个dump.rdb文件,它存储了redis中的数据
使用vi命令查看文件中的数据:
REDIS0009ú redis-ver^E6.2.6ú
redis-bitsÀ@ú^EctimeÂ5\£eú^Hused-memÂH}^M^@ú^Laof-preambleÀ^@þ^@û^A^@^@^Baa^Bbbÿ^_^B9QYbjx
在文件的最后,可以看到刚才存储的aa、bb
总结
通过这个入门案例,简单地了解一下redis的基本特性和日常使用。可以知道:
- redis中存储键值对
- redis中的数据会被备份到磁盘
基本操作
redis是以键值对存储数据的,在redis中,键总是一个字符串,而值可以是redis支持的任意数据类型。
键的命名规则:多个单词使用冒号分割
注意事项:
- 不要把key设的太长,否则检索key的时候字符串比较会很耗时,如果字符串太长,可以对它做一个哈希计算
- 键和值的大小不可以超过512M
数据类型
redis中有5个最常用的数据类型:字符串、列表、集合、有序集合、哈希。
字符串
string,字符串可以不加引号,默认以空格分割,如果字符串中包含空格,必须加引号
字符串是最基本的类型,而且字符串是二进制安全的,意思是redis的字符串可以包含任何数据,比如 jpg 图片或者序列化的对象。从内部实现来看其实字符串可以看作字节数组,最大上限是 1G 字节。
操作字符串:
- 存储和取出单个键值对:set、get
- 一次存入多个键值对:mset命令,格式:
mset key value [key value ....]
- 一次取出多个键值对:mget命令,格式:
mget key [key ...]
- 将键所对应的数字值加1:incr命令,格式:
incr key
,如果值不是一个数字则报错 - 操作键所对应的数字值:
- 自减:decr
- 自增指定数字:incrby
- 自减指定数字:decrby
- 自增浮点数:incrbyfloat
- 在值得后面追加字符串:append,格式:
append key value
- 键所对应的值的长度:strlen:格式:
strlen key
- 更新键的值并且返回原值:getset,格式:
getset key value
- 设置指定偏移量位置的值,偏移量从零开始:setrange,格式:
setrange key offset value
- 获取指定范围的字符串的值:getrange,格式,
getrange key start end
列表
list,redis的list底层基于链表
操作列表:
- 把新元素添加到列表的开头:lpush命令,格式:
lpush key element [element ...]
- 把新元素添加到列表的结尾:rpush命令,格式:
rpush key element [element ...]
- 取出列表中一定范围的值:lrange命令,格式:
lrange key start stop
,下标从0开始 - 获取列表指定索引下标的元素:lindex命令,格式:
lindex key index
,下标从0开始 - 向指定位置插入元素:linsert命令,格式:
linsert key BEFORE|AFTER pivot element
,pivot是列表中的元素,表示在哪个元素的前面或者后面插入值 - 获取列表的长度:llen命令,格式:
llen key
- 从列表左侧弹出元素:lpop命令,格式:
lpop key [count]
,弹出元素后元素将不存在与列表中- blpop:lpop 的阻塞版
- 从列表右侧弹出元素:rpop命令,格式:
rpop key [count]
,弹出元素后元素将不存在与列表中- brpop:rpop 的阻塞版
- 删除指定元素:lrem命令,格式:
lrem key count element
,- 当参数count等于0时,该命令会移除列表中所有值为 element 的元素
- 当参数 count 大于0时,该命令会列表头开始向列表尾遍历,删除 count 个值为 element 的元素
- 保留指定区间的元素:ltrim命令,格式:
ltrim key start stop
,下标在start和stop之间的元素将会被保留
列表的常用功能:使用列表来模拟队列、栈、阻塞队列
集合
set,无序、不可重复,集合的底层是哈希表,存储string类型的数据,对于集合的操作通常有计算并集、交集、差集等
操作集合:
- 向集合中添加数据:sadd命令,格式:
sadd key member [member ...]
- 查看集合中的所有元素:smembers命令,格式:
smembers key
- 计算集合中元素个数:scard命令,格式:
scard key
- 判断元素是否在集合中:sismember 命令,格式:
sismember key member
,如果元素存在于集合中,返回1 - 删除集合中的元素:srem命令,格式:
srem key member [member ...]
- 求交集:sinter命令,格式:
sinter key [key ...]
- 求并集:sunion命令,格式:
sunion key [key ...]
- 求差集:sdiff命令,格式:
sdiff key [key ...]
,求第一个集合中有二第二个集合中没有的元素 - 将交集、并集或差集并保存结果:
sinterstore
、sunionstore
、sdiffstore
。格式:sinterstore destination key [key ...]
有序集合
sorted set,又称zset,set集合的有序版,
- 每个元素都会关联一个double类型的数,
- redis正是通过它来为集合中的成员进行排序,
- 有序集合的成员是唯一的,但成员关联的数可以重复。
操作有序集合:
- 向有序集合中添加元素:zadd命令,格式:
zadd key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]
- 查看有序集合中的元素:zrange命令,格式:
zrange key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
- 成员个数:zcard命令,格式:
zcard key
- 某个成员的分数:zscore命令,格式:
zscore key member
- 删除成员:zrem,格式:
zrem key member [member ...]
哈希
哈希数据类型中存储的是键值对,要求键值对都是string类型,可以理解为一个对象,哈希类型的数据:key: {field, value, ...}
操作hash:
- 存储hash类型的数据:hset命令,格式:
hset key field value [field value ...]
- 取出hash类型的数据:hget命令,格式:
hget key field
- 删除某个字段:hdel命令,格式:
hdel key field [field ...]
- 计算字段的个数:hlen命令,格式:
hlen key
- 判断某个字段是否存在:hexists命令,格式:
hexists key field
- 获取所有的字段:hkeys命令,格式:
hkeys key
- 获取所有的值:hvals命令,格式:
hvals key
- 获取所有的字段和值:hgetall命令,格式:
hgetall key
,在返回的数据中,第一行是字段,第二行是字段的值,依此类推 - 对字段的值做数学计算:
hincrby
、hincrbyfloat
- 计算值的字符串长度:
hstrlen
命令的帮助文档
在命令行中输入 help <命令>,可以查询某个命令的帮助文档
常用命令
查看服务器的信息
1、获取redis服务器的统计信息:info
2、返回当前的服务器时间:time
,返回一个时间戳
3、实时打印出redis服务器接收到的操作数据的命令:monitor
4、配置项
-
获取某个配置项的值:
config get 配置项
-
设置某个配置项的值:
config set 配置项 值
5、获取客户端列表:client list
操作数据库
清除数据库:flushdb
、flushall
键迁移:move
切换数据库:select
登录和退出
输入密码,登录:auth
同步保存到服务器并关闭redis服务器:shutdown
redis的常见操作
- 存入和读取字符串:set、get命令,多次set一个键,后面的 set 命令将会覆盖前面的set命令
- 删除键:del命令,格式:del key [key …],成功返回 1 否则返回 0
- 设置键的过期时间:expire命令,格式:expire key seconds
- 查看键的剩余存活时间:ttl命令,格式:ttl key
- 返回-1,表示键没有设置存活时间,键会一直存在,
- 返回-2,表示键已经不存在,
- 返回一个整数,表示键还可以存活多少秒
- 查询键是否存在:exists命令,格式:exists key [key …],如果键不存在,返回0
- 查询键所对应的值的类型:type命令,格式:type key
- 重命名键:rename命令,格式:rename key newkey
- 随机返回一个键: randomkey命令
- 查询所有的键:keys * ,* 是一个模式,匹配所有。keys命令的格式:keys pattern,使用 * 表示匹配所有
- 查看当前库下键的个数:dbsize
基本配置
在之前启动redis服务时,使用的是默认配置,但是实际生成过程中,通常会对redis做一些简单的配置,包括后台运行、登录验证、日志等。
redis.conf:redis的核心配置文件
查看文件中的配置信息:cat ${REDIS_HOME}/conf/redis.conf | grep -v '^#' | grep -v '^$'
,因为文件中有很多注释,如果想要快速查看配置信息,需要把这些注释过滤掉。
redis.conf中的配置项
指定其它配置文件的位置:include /path/to/local.conf
- 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
配置redis服务器的行为:
-
服务器是否以守护进程的形式运行:
daemonize no
:值可以为 yes 或 no,配置 redis-server 进程是不是一个守护进程。 -
保护模式:
protected-mode yes
:是否开启保护模式,默认开启。要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问。 -
服务器只处理来自指定IP地址请求:
bind 127.0.0.1
:redis 服务器只接收来自指定IP地址的请求,默认可以接收所有IP地址的请求 -
服务器绑定的端口:
port 6379
: 指定redis服务器运行的端口,默认是6379 -
设置密码:
requirepass farbar
:只要是来自指定服务器的请求,并且密码通过校验,就可以访问服务器。如果配置了连接密码,客户端在连接 Redis 时需要通过auth <password>
命令提供密码,默认关闭 -
服务器所在进程pid文件的位置:
pidfile /var/run/redis.pid
:pidfile文件中存储了redis实例的进程ID。当redis服务器以守护线程的形式运行的时候,Redis默认会把pid文件放在pidfile参数指定的位置。 -
内存限制:
maxmemory <bytes>
:指定 Redis最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
配置客户端连接:
-
客户端连接超时时间:
timeout 0
:设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接,0是关闭此设置 -
同一时间最大客户端连接数:
maxclients 10000
,设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息 -
客户端TCP连接保活策略:
tcp-keepalive 300
:单位为秒,server端会经过单位时间就会向连接空闲的客户端发起一次ACK请求,以检查客户端是否已经挂掉,对于无响应的客户端则会关闭其连接。为0表示关闭
配置日志记录
-
日志级别:
loglevel notice
: Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice。-
四个日志级别:
-
debug :记录很多信息,用于开发和测试;
-
varbose:记录有用的信息,不像debug会记录那么多
-
notice 普通的verbose,常用于生产环境;
-
warning 只有非常重要或者严重的信息会记录到日志
-
-
-
日志文件:
logfile stdout
:默认认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null。如果没有值,默认为标准输出- 配置日志文件时的注意事项:
- 不能引用环境变量:
logfile ${REDIS_HOME}/logs/redis.log
,这个是错误的 logfile ./redis.log
,在这项配置中,“.”相当于命令执行目录- 如果想把配置文件放到指定目录下,在这里建议使用绝对路径
- 不能引用环境变量:
- 配置日志文件时的注意事项:
配置数据库
- 设置数据库数量:
databases 16
:对于redis来说,可以设置其数据库的总数量,这16个数据库的编号将是0到15。默认的数据库是编号为0的数据库。用户可以使用select <DBid>
来选择相应的数据库
配置redis允许远程连接
bind 0.0.0.0
protected-mode no
需要修改的配置项
daemonize yes
:reids服务器以守护线程的方式运行
requirepass 123456
:设置密码
logfile /opt/redis/logs/redis.log
:设置日志文件的位置,这只是普通的日志文件,不是持久化日志文件
bind 0.0.0.0
、protected-mode no
:允许远程连接
dir /opt/redis-6.2.6/data
:指定磁盘数据的存放目录
redis开启和关闭密码验证
开启:
- 设置redis.conf中requirepass的值是一个字符串,这个字符串就是密码,
- 启动服务器,进入客户端,使用auth <密码> 的形式进行登录,或者可以在命令行使用redis-cli命令配合-a选项进行登录,但是在命令行输入密码不安全。
关闭:
- 设置redis.conf中requirepass的值是一个空字符串,然后重启redis服务
总结
在之前“数据类型”章节中,演示了每种数据类型的基本使用,不过没有提供它们的使用案例。在日常工作中,最经常使用的还是字符串类型。键值对的存储和获取是redis提供的基本功能,除此以外,redis还提供了持久化、删除过期key、集群等功能,这些功能让redis能够更好地应用于缓存方向。
深入使用
键的过期删除策略
redis中的键默认是不过期的,但是可以为键设置过期时间,键会在指定的时间之后过期,过期后则会被删除。
redis提供了不同的删除策略,以满足不同的要求:
-
定时删除:创建一个定时器,到时间立即执行删除操作,它对内存友好,因为能保证过期了立马删除,但是对cpu不友好
-
惰性删除:键过期不管,每次获取键时检查是否过期,过期就删除,它对cpu友好,但是只有在使用的时候才可能删除,但是对内存不友好
-
定期删除:redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。从过期字典中随机20个key,删除这20个key中已经过期的key;如果过期的key比率超过 1/4,那就重复步骤1;同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
redis采用 惰性删除 + 定期删除 的策略:
-
访问一个键时,会先判断键有没有过期,若没过期,直接执行相应操作,
-
同时redis会在指定的时间内检查是否有过期的键,采用贪心策略来删除key。
案例:存储键值对时设置key的过期时间
-- 设置一个key,10秒后过期
127.0.0.1:6379> set k1 v1 EX 10 NX
OK-- 查看key的过期时间,这里表示还剩7秒
127.0.0.1:6379> ttl k1
(integer) 7-- 10秒后读取key,发现key不存在了
127.0.0.1:6379> get k1
(nil)
设置key的过期时间使用的命令:set命令:SET key value[EX seconds][PX milliseconds][NX|XX]
,它操作的键值对都是字符串
-
EX seconds:设定key的过期时间,时间单位是秒。
-
PX milliseconds: 设定key的过期时间,单位为毫秒
-
NX:表示key不存在的时候,才能set成功
-
XX:仅当key存在时设置值,和NX正好相反
持久化机制
redis是基于内存的数据库,它支持把内存中的数据保存到磁盘上,进行持久化存储。
redis支持RDB和AOF两种持久化机制
RDB
RDB:Redis DataBase,数据快照,在不同的时间点,将redis存储的数据生成快照并存储到磁盘上。redis会单独创建一个子进程来进行持久化,这确保了redis性能。
在RDB的机制中,决定什么时候持久化的条件是:在多长时间内有多少数据发生了改变
RDB的配置项
1、设置RDB的同步行为:save <seconds> <change>
:
- 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件。
- 如果想禁用RDB持久化的策略,只要不设置任何save指令就可以,或者给save传入一个空字符串。
Redis默认配置文件中提供了三个条件:
save 900 1
:900 秒(15 分钟)内有 1 个更改save 300 10
:300 秒(5 分钟)内有 10 个更改save 60 10000
:60 秒内有 10000 个更改
2、RDB失败时的处理方式:stop-writes-on-bgsave-error yes
:
- 如果用户开启了RDB快照功能,那么在redis持久化数据到磁盘时如果出现失败,默认情况下,redis会停止接受所有的写请求。
- 这样做的好处在于可以让用户很明确的知道内存中的数据和磁盘上的数据已经存在不一致了。
- 如果下一次RDB持久化成功,redis会自动恢复接受写请求。
- 如果不在乎这种数据不一致或者有其他的手段发现和控制这种不一致的话,完全可以关闭这个功能,以便在快照写入失败时,也能确保redis继续接受新的写请求
3、是否压缩RDB的快照文件:rdbcompression yes
:
- 对于存储到磁盘中的快照,可以设置是否进行压缩存储。
- 如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
4、是否对RDB的快照文件进行数据校验:rdbchecksum yes
:
- 在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
5、设置RDB快照文件的存放路径:dir ./
,默认和配置文件一样,存放在redis的根目录之下
6、设置RDB快照文件的名称:dbfilename dump.rdb
,默认快照文件的名称是dump.rdb
配置案例
dbfilename dump.rdb -- 配置rdb文件的名称
dir /opt/redis-6.2.6/data -- 配置rdb文件的存储路径
在命令行操作RDB
查看配置项save的默认值:config get save
,值是:“3600 1 300 100 60 10000”,表示在3600秒内发生一次改变,或在300秒内发生100次改变,或在60秒内发生10000次改变,就会触发RDB操作
手动触发RDB操作:
- 同步地备份快照数据到磁盘:save命令
- 异步地备份快照数据到磁盘:bgsave命令
AOF
AOF:Append Only File,日志记录,只允许追加不允许改写的文件。
工作机制:
- 将redis执行过的所有写指令记录下来,
- 在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
默认的AOF持久化策略是每秒钟把缓存中的写指令记录到磁盘中一次,在这种情况下,redis仍然可以保持很好的处理性能,即使redis故障,也只会丢失最近1秒钟的数据。
AOF的重写机制:AOF采用了追加方式,如果不做任何处理的话,AOF文件会变得越来越大,为此,redis提供了AOF文件重写机制,当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。例如,假如调用了100次INCR指令,在AOF文件中就要存储100条指令,但这明显是很低效的,完全可以把这100条指令合并成一条SET指令,这就是重写机制的原理。
AOF配置项
1、开启AOP:appendonly no
:是否开启AOF机制,默认是关闭的
2、AOF文件名:appendfilename appendonly.aof
。指定更新日志文件名,默认为 appendonly.aof
3、AOF更新条件:appendfsync everysec
。指定更新日志条件,共有 3 个可选值:
- no:表示等操作系统进行数据缓存同步到磁盘(快)
- always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)
- everysec:表示每秒同步一次,这是默认值
4、配置什么时候启动AOF的重写机制:
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
这两个参数共同限制了重写机制的执行时机,如果当前文件大小超过64M,并且当前AOF文件的增长量大于上次AOF文件的100%,就执行重写机制
配置案例
appendonly yes -- 开启aof机制
appendfilename "appendonly.aof" -- aof文件的名称
dir /opt/redis-6.2.6/data -- aof文件的存放路径
appendfsync everysec -- 每秒同步一次指令
在命令行操作AOF
重写aof文件:bgrewriteaof命令,在后台异步地重写aof文件
RDB和AOF的对比
它们都是redis提供的持久化机制,这里从性能和安全的角度来对比这两种持久化
-
性能:
- RDB的性能优于AOF,使用RDB恢复数据会更快。RDB因为不需要向AOF一样需要对磁盘文件进行重写,所以性能更好
-
安全:
-
RDB在数据的完整性上不如AOF,使用RDB,即使5分钟就持久化一次,如果redis出现故障,仍然有近5分钟的数据会丢失,
-
使用AOF,只会丢失一秒钟的数据
-
数据的发布和订阅
Redis 发布订阅是一种消息通信模式,发送者发送消息,订阅者接收消息,Redis 客户端可以订阅任意数量的频道。用于在多个redis客户端之间传递数据
订阅消息:subscribe命令,格式:subscribe channel [channel ...]
,订阅的时候这个频道不需要提前存在,订阅者也无法接收到这个频道之前的消息
发布消息:publish命令,格式:publish channel message
,向指定频道发布消息
事务
Redis的事务并不是原子性的,事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
redis中执行事务的命令:
-
multi:开启事务
-
任意redis命令
-
exec:执行multi之后的所有命令
-
discard:放弃执行事务块内的所有命令
redis事务的执行机制:
-
批量操作在发送 EXEC 命令前被放入队列缓存,
-
收到 EXEC 命令后进入事务执行,
-
事务中任意命令执行失败,其余的命令依然被执行,
-
在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中
搭建redis集群
为什么要使用redis集群:单个redis的读写能力有限并且不稳定性,所以需要使用redis集群强化redis的读写能力。
redis集群的架构:集群中的节点分为master和slave两种,一个master对应一个slave,master负责读写请求,slave负责读请求和备份master上的数据,集群是去中心化的。
架构详解:
-
redis集群中,每一个redis称之为一个节点,有两种类型的节点:主节点(master)、从节点(slave)
-
一个主节点至少要有一个从节点对它的数据进行备份,从节点只对外提供读服务,并且可以考虑关闭主节点的持久化功能,由从节点来对数据进行备份。
-
redis集群是基于主从复制实现的,采用P2P模式,是完全去中心化的,不存在中心节点或者代理节点;
-
redis集群是没有统一的入口的,客户端(client)连接集群的时候连接集群中的任意节点(node)即可,集群内部的节点是相互通信的(PING-PONG机制),每个节点都是一个redis实例。
集群的工作机制:主要介绍两个点,如何判断master节点是否可用?投票容错机制,如何判断数据该存储到哪个节点上?哈希槽,
-
投票容错机制:为了实现集群的高可用,即判断节点能否正常使用,redis-cluster有一个投票容错机制,如果集群中超过半数的节点投票认为某个节点挂了,那么这个节点就挂了(fail)。如果集群中任意一个节点挂了,而且该节点没有从节点(备份节点),那么这个集群就挂了。
-
哈希槽:集群内置了16384个哈希槽(slot),并且把哈希槽映射到不同的物理节点上,当需要在redis集群存放一个数据时,redis会先对这个key求key的哈希值,然后得到一个结果,再把这个结果对16384进行求余,这个余数会对应[0-16383]其中一个槽,进而决定key-value存储到哪个节点中。通过这种哈希槽机制,实现了数据和节点在逻辑上的分离。
集群所需服务器:redis集群至少需要6个节点,3个主节点和3个从节点。因为投票容错机制要求超过半数节点认为某个节点挂了该节点才是挂了,所以2个节点无法构成集群。要保证集群的高可用,需要每个节点都有从节点,也就是备份节点,所以Redis集群至少需要6台服务器。
实战-搭建redis集群
一个redis集群至少需要有6个节点,也就是至少需要六台服务器,因为没有这么多服务器,所以把所有的redis实例都放到同一个服务器上,在这里搭建一个伪分布式集群,它们使用7001-7006端口
-
第一步:新建redis-cluster目录,用于存放redis的代码,将之前安装好的redis复制到新建目录下,命名为redis01
-
第二步:进入redis01目录下,删除之前可能存在的、不需要的文件:
rm appendonly.aof dump.rdb redis.log
-
第三步:修改配置文件redis.conf,具体修改的配置项和修改后的值:
-
配置服务器以守护线程的方式运行:
daemonize yes
-
配置服务器可以处理任意节点的请求:
bind * -::*
-
配置redis服务器的运行端口:
port 7001
-
配置密码:
requirepass 123456
-
配置pidfile的存放位置:
pidfile ./redis_7001.pid
-
配置logfile的文件名:
logfile ./redis.log
-
配置快照文件的存放路径:
dir ./
-
配置开启集群模式:
cluster-enabled yes
-
-
第四步:将redis01复制5份,分别命名为redis02到redis06
-
第五步:修改redis02到redis06中的配置,只修改port和pidfile的值即可
-
sed -i 's/port 7001/port 7002/' redis01/redis.conf
-
sed -i 's/redis_7001.pid/redis_7002.pid/' redis01/redis.conf
-
-
第六步:启动redis:需要先cd到redis根目录下,然后启动redis,因为redis中的相对路径是相对于命令运行时的路径。
-
第七步:查看每个节点的启动情况:ps aux | grep redis
-
第八步:创建集群:
./redis01/src/redis-cli \
-a 123456 \
--cluster create 192.168.0.3:7001 192.168.0.3:7002 192.168.0.3:7003 192.168.0.3:7004 192.168.0.3:7005 192.168.0.3:7006 \
--cluster-replicas 1
-
第九步:查看结果:进行redis任意一个实例的根目录,查看nodes.conf文件,可以看到集群的配置信息
-
第十步:连接redis集群:
./src/redis-cli -c -h 192.168.0.3 -p 7002 -a 123456
。-
-c表示客户端开启集群模式,
-
-h指定redis服务进程所在的主机名,
-
-p指定redis服务进程的端口,
-
-a参数指定密码
-
安装早期版本的redis时需要ruby
在比较早的redis版本中,搭建集群需要ruby。因为要搭建集群的话,需要使用一个工具(脚本文件),这个工具在redis解压文件的源代码里。因为这个工具是一个ruby脚本文件,所以这个工具的运行需要ruby的运行环境,
ruby的安装过程:
-
下载ruby的安装包:
wget https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz
-
解压:
tar -zvxf ruby-2.4.5.tar.gz
-
进入根目录,执行命令:
./configure、make、make install
-
配置ruby的环境变量:ruby被安装到了 /usr/local/bin 目录下,把这个目录配置到PATH变量中
-
验证安装结果:
ruby -v
集群的常用操作
在客户端查看集群信息
打印集群的信息:cluster info
打印集群节点信息:cluster nodes
增删节点
移除某个节点:cluster forget <node_id>
,node_id是 cluster nodes 命令中打印的随机字符串
操作集群上的键的命令
计算某个键应该被放到哪个槽上:cluster keyslot <key>
,返回一个槽ID
操作主从节点的命令
slaveof <masterip> <masterport>
:
-
Redis提供了主从功能,通过salveof配置项可以控制一台redis服务器作为另一个redis的从服务器,通过指定的IP和端口来确认对应的服务器。
-
一般情况下,会建议用户为从redis设置一个不同频率的快照持久化的周期,或者为从redis配置一个不同的服务端口等等。
masterauth <master-password>
:
- 如果主redis设置了验证密码的话(使用requirepass来设置),则在从redis的配置中要使用masterauth来设置校验密码,否则的话,主redis会拒绝从redis的访问请求。
常用命令
redis集群清理数据:./src/redis-cli --cluster call 192.168.0.3:7001 flushall
redis集群查看每个节点的数据大小:./src/redis-cli --cluster call 192.168.0.3:7001 dbsize
关闭redis服务:./src/redis-cli -h 192.168.0.3 -p 7006 -a 123456 shutdown
集群的工作机制
主从同步原理
从服务器向主服务器同步数据,分两步走:
-
从服务器初始化时同步数据:
-
从服务器初始化时会向主服务器发送同步指令,当主服务器接收到此指令后,会将主服务器的数据写入rdb文件中,
-
在数据持久化期间,主服务器将执行的写指令都缓存在内存中。写完后,主服务器会将持久化好的rdb文件发送给从服务器,
-
从服务器接收到此文件后会将其存储到磁盘上,然后再将其读取到内存中。
-
-
从服务器初始化完成后向主服务器同步数据:这个动作完成后,主服务器会将这段时间缓存的写指令再以redis协议的格式发送给从服务器。
redis的通信协议:RESP协议
redis序列化协议,Redis Serialization Protocol,是服务端和客户端之间的通信协议,它的特点是解析快、易于阅读,底层是基于TCP的
实战案例
springboot整合redis
需求:为springboot项目中的某些接口添加缓存,使用redis来存储缓存数据
实现步骤:使用springboot提供的关于redis的启动器,添加redis相关的配置,在代码中使用RedisTemplate来访问redis
第一步:添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.1.5.RELEASE</version>
</dependency>
第二步:在application.properties中添加redis相关的配置
# redis的配置信息
spring.redis.host=ip地址
spring.redis.port=6379
spring.redis.timeout=5000
第三步:使用redis,这里是一个切面类,使用redis作为缓存。它会拦截被@Cache标注的方法,把该方法的返回值放到缓存中。在这里使用RedisTemplate作为客户端和redis交互。
@Aspect
@Component
@Order(2)
public class CacheAspect {private static final Logger LOG = LoggerFactory.getLogger(CacheAspect.class);private static final String DOUBLE_COLON = "::";@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Pointcut("@annotation(org.wyj.blog.aop.cache.Cache)")public void pt() { }@Around("pt()")public Object around(ProceedingJoinPoint joinPoint) throws Exception {// 获取拦截点的方法信息String className = joinPoint.getTarget().getClass().getSimpleName();Object[] args = joinPoint.getArgs();String paramStr = Arrays.toString(args);MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();String methodName = method.getName();Cache cacheAnnotation = method.getAnnotation(Cache.class);long expire = cacheAnnotation.expire();String cacheName = cacheAnnotation.name();// 拼接key,包括缓存名、方法名、方法参数String redisKey = cacheName + DOUBLE_COLON + className + DOUBLE_COLON +methodName + DOUBLE_COLON + paramStr;// 尝试从redis中获取数据Object redisValue = redisTemplate.opsForValue().get(redisKey);if (redisValue != null) {LOG.info("返回缓存: {}.{}({})", className, methodName, paramStr);return redisValue;}// 执行连接点的方法Object result;try {if (args.length > 0) {result = joinPoint.proceed(args);} else {result = joinPoint.proceed();}} catch (Throwable e) {throw new Exception(e);}// 向redis中存入数据redisTemplate.opsForValue().set(redisKey, (String)result, Duration.ofSeconds(expire));LOG.info("存入缓存: {}.{}({})", className, methodName, paramStr);return result;}
}
使用redis作为缓存
为什么使用缓存:在日常的web应用对数据库的访问中,读操作的次数远超写操作,所以需要读的可能性是比写的可能大得多的,这种情况下使用缓存可以提升程序的响应性能。
使用缓存的考虑:业务数据常用吗?命中率如何;该业务数据是读操作多,还是写操作多;业务数据大小如何
使用缓存会出现的问题:缓存穿透、缓存雪崩、缓存和数据库双写一致性问题。
-
缓存雪崩:同一时间内大量数据失效,导致原本访问 redis 的请求全部去到数据库,造成数据库短期内 cpu 和内存压力激增,严重时甚至造成宕机。
-
缓存穿透:查询一个不存在于缓存中的数据,如果同时又许多这种查询,造成数据库压力激增,叫做缓存穿透
-
缓存和数据库双写一致问题:同时更新缓存 和数据库的原子性无法得到满足。可以利用消息队列来实现最终一致性的保证,并且还需要利用消息队列的重试机制来保证更新成功
缓存失效应该是有固定条件的,取决于缓存方案,如果数据写操作不多,那么可以考虑给缓存设置一个较大的过期时间,如果数据变化了,那么需要精准地让缓存中的相关数据失效,避免延迟,因为大量缓存同时失效造成的缓存雪崩,需要从业务场景上来分析,什么操作导致的缓存大量失效?
缓存穿透可以考虑优化缓存数据,把经常访问的数据放到缓存中。
缓存雪崩和缓存穿透我在实际开发中遇到的比较少,网上提供了多种解决方案,但是我觉得比较常见的方案的就是数据库限流。
使用redis实现分布式锁
分布式锁:用于在多个进程之间实现同步的一种锁
为什么要使用分布式锁?为了保护共享资源,在分布式系统中,一个请求是在某一台机器上处理的,如果多个用户同时提交请求,处理共享资源,需要使用分布式锁来保证共享资源的安全。
分布式锁的要求:
-
互斥性:任意时刻,只有一个客户端持有锁
-
超时释放:在指定时间后锁自动释放,避免程序崩溃带来的死锁
-
可重入:一个进程获取锁之后,可以再次对其请求加锁
-
安全性:锁只能被持有的客户端删除,不能被其它客户端删除
-
高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
实现原理:多个服务同时在redis中创建一个键,值是每个进程特有的,只有一个会创建成功,相当于获取到锁,其余的进程等待获取到锁的进程释放锁,获取锁的进程执行完任务后,释放锁,其余进程继续尝试获取锁。
案例:
@Component
public class DistributeLock {private static final Logger LOG = LoggerFactory.getLogger(DistributeLock.class);@Autowiredprivate RedisTemplate<String, String> redisTemplate;private final ThreadLocal<Integer> distributeLockThreadLocal = new ThreadLocal<>();/*** 获取分布式锁** @param expireTime:键过期时间* @param waitTime:等待时间,如果获取锁失败,等待指定时间后,再次获取,可以设为0*/public boolean acquireLock(String lockKey, String requestId, long expireTime, TimeUnit timeUnit, long waitTime) {ValueOperations<String, String> ops = redisTemplate.opsForValue();Integer lockCount = distributeLockThreadLocal.get(); // 可重入的逻辑if (lockCount != null) {lockCount++;distributeLockThreadLocal.set(lockCount);LOG.info("获取到分布式锁 {} {} 重入次数 {}", lockKey, requestId, lockCount);return true;}boolean result = Boolean.TRUE.equals(ops.setIfAbsent(lockKey, requestId, expireTime, timeUnit));if (!result && waitTime > 0) { // 增加等待时间try {Thread.sleep(timeUnit.toMillis(waitTime));} catch (InterruptedException e) {throw new RuntimeException(e);}result = Boolean.TRUE.equals(ops.setIfAbsent(lockKey, requestId, expireTime, timeUnit));}if (result) {LOG.info("获取到分布式锁 {} {}", lockKey, requestId);distributeLockThreadLocal.set(1);}return result;}/*** 释放分布式锁*/public void releaseLock(String lockKey, String requestId) {ValueOperations<String, String> ops = redisTemplate.opsForValue();String value = ops.get(lockKey);if (requestId.equals(value)) { // 当前进程持有锁Integer lockCount = distributeLockThreadLocal.get(); // 可重入的逻辑if (lockCount > 1) {lockCount--;distributeLockThreadLocal.set(lockCount);LOG.error("释放锁成功 {} {} 重入次数 {}", lockKey, requestId, lockCount + 1);} else {distributeLockThreadLocal.remove();boolean result = Boolean.TRUE.equals(redisTemplate.delete(lockKey));if (result) {LOG.error("释放锁成功 {} {}", lockKey, requestId);}}}}
}
总结:案例中实现了获取分布式锁和释放分布式锁的基本流程,并且分布式锁是可重入的。基本功能包括 获取锁的时候可以设置等待时间、支持设置redis中key的过期时间,注意,释放锁时先判断是否是当前线程持有锁,否则逻辑就有问题。可重入是基于ThrreadLocal,所以不支持同一个进程中跨线程获取锁
分布式锁的使用:
@RestController
@RequestMapping("/book")
public class BookController {private static final Logger LOG = LoggerFactory.getLogger(BookController.class);@Autowiredprivate DistributeLock distributeLock;@GetMapping("/lock")public String lock(@RequestParam String name) {String value = UUID.randomUUID().toString(); // 生成一个代表当前操作的唯一id,释放锁时需要用到// 获取锁boolean acquireLock = distributeLock.acquireLock(name, value, 10, TimeUnit.SECONDS, 3);if (acquireLock) {// 模拟业务逻辑的执行try {Thread.sleep(3 * 1000);} catch (InterruptedException e) {throw new RuntimeException(e);}// 再次获取锁acquireLock = distributeLock.acquireLock(name, value, 10, TimeUnit.SECONDS, 3);if (acquireLock) {try {Thread.sleep(3 * 1000);} catch (InterruptedException e) {throw new RuntimeException(e);}distributeLock.releaseLock(name, value);}distributeLock.releaseLock(name, value); // 释放锁}return name + " " + value;}
}
这里是一个简单的使用案例,获取锁,并且设置获取锁时等待3秒,key的过期时间是10s,执行完业务逻辑后释放锁。
bigKey问题和bigValue问题
bigKey问题
- 概念:一个key对应的数据结构,如list、set、hash,整体很大,例如,一个key下存储了一个包含百万个元素的列表。bigKey更加关注元素数量和内存消耗,处理bigKey问题时更加关注如何分片和分解数据结构
- 危害:如果一个key对应的值比较大,在cpu计算、网络带宽方面的消耗也会比其它key多,redis是单线程,处理单个key的时间如果比较长,会阻塞其它请求
- 产生原因:大key在cpu计算、网络带宽方面的消耗会比其它key多
- 解决方案:
- 把一个key对应的值拆分到多个key中,可以在key上使用哈希分桶。
bigValue:一个key对应的单个元素元素值本身很大,例如,一个key对应的值是一个大字符串,bigValue更加关注单个数据块的传输效率,处理bigValue问题通常是数据压缩或者把数据拆分到多个key中。
踩坑记录
1、启动redis时遇到的错误:HandleServiceCommands: system error caught. error code=1056, message = StartService failed: unknown error。
因为当前redis服务已经启动,无需再次启动。在Windows上使用redis时遇到了这个报错
2、使用redis过程中碰到的问题:Cannot assign requested address redis:无法分配请求的地址。
因为客户端频繁的连服务器,由于每次连接都在很短的时间内结束,导致很多的TIME_WAIT,以至于用光了可用的端口号,所以新的连接没办法绑定端口,即“Cannot assign requestedaddress”。建议使用连接池来复用连接。