文章目录
一、Redis基础
1.1、安装Redis
1.1.1 Windows安装Redis
下载zip包:https://github.com/MicrosoftArchive/redis/releases
zip解压后主要文件说明:
redis-server.exe----Reds服务
redis-cli.exe------Redis客户端
redis.windows.conf-----Redis配置
修改配置:(修改redis.windows.conf)
#允许其他ip访问本机:
bind 0.0.0.0
#设置密码:(默认没有启用密码)
requirepass 123456
启动:
启动服务:
打开cmd,切换到zip解压后的目录
执行:redis-server.exe redis.windows.conf
(Windows PowerShell中使用: .\redis-server.exe redis.windows.conf )
关闭命令行窗口,服务即关闭!!!
启动客户端:
执行:redis-cli -h 127.0.0.1 -p 6379
用户登录:127.0.0.1:6379> auth [password]
注册Redis服务:
方式一:使用cmd,切换到Redis目录:
执行:redis-server.exe --service-install redis.windows.conf --loglevel notice --service-name Redis
方式二:使用Windows PowerShell,切换到Redis目录:
执行: .\redis-server.exe --service-install redis.windows.conf --loglevel notice --service-name Redis
启动服务:
打开任务管理器,切换到服务,找到Redis服务,启动即可。
通过注册服务后,以后就可以开机启动了。
可视化界面工具:
redis-desktop-manager
IntelliJ IDEA 安装 Redis插件后,也可以直接连接Redis
1.1.2 Linux安装
linux系统安装Redis需要依赖gcc
查看gcc是否安装(查看gcc版本):gcc --version
如果未安装,搜索gcc安装包:yum search gcc
是用包名下载:yum install gcc-c++
下载:
官方下载地址:http://download.redis.io/releases/
下载Redis6:
cd /root
wget -p /root/Download http://download.redis.io/releases/redis-5.0.5.tar.gz
wget -p /root/Download http://download.redis.io/releases/redis-6.2.7.tar.gz
切换到压缩包目录下:cd /root//root/Downloads/download.redis.io/releases
解压:tar -zxvf redis-5.0.5.tar.gz -C /usr/local
编译(在/usr/local/redis-5.0.5下):make
安装:
进入到src下:cd src
安装(默认安装到/usr/local/bin目录下):make install
拷贝编译生成的相关可执行文件:为了以后快速启动
创建文件夹:
存放redis的执行文件:mkdir -p /usr/local/redis/bin
存放redis的配置文件:mkdir -p /usr/local/redis/etc
拷贝文件:(进入redis解压目录中)
cp redis.conf /usr/local/redis/etc
cp sentinel.conf /usr/local/redis/etc
cp src/redis-server /usr/local/redis/bin
cp src/redis-benchmark /usr/local/redis/bin
cp src/redis-cli /usr/local/redis/bin
cp src/redis-check-aof /usr/local/redis/bin
cp src/redis-check-rdb /usr/local/redis/bin
cp src/mkreleasehdr.sh /usr/local/redis/bin
cp src/redis-sentinel /usr/local/redis/bin
修改配置:
修改配置信息:vim redis.conf
后台启动:daemonize yes
数据文件保存位置:dir /usr/local/redis/etc/
允许跨网访问:bind 0.0.0.0
设置密码:(局域网内使用可以不使用密码)
修改 redis.conf配置文件,设置:requirepass [密码]
重启redis服务
进入客户端:/usr/local/redis/bin/redis-cli -a [密码]
进入客户端(方式二):/usr/local/redis/bin/redis-cli
再输入:auth [密码]
手动启动Redis:
手动启动服务:(如过配置了redis.server文件可以直接使用:systemctl start redis.service )
启动redis-server:/usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf
查看服务是否启动:ps -ef |grep redis
关闭redis服务:pkill redis-server
关闭redis服务:/usr/local/redis/bin/redis-cli shutdown
启动客户端:./redis-cli
测试:
设置key-value:set [key] [value]
通过key获取值:get [key]
删除key-value:del [key]
退出客户端:quit
linux自动启动Redis:
自动启动服务:(CentOS7)
创建服务文件:
进入系统服务文件目录下:cd /usr/lib/systemd/system
创建服务文件:touch redis.service
编辑文件:vim /usr/lib/systemd/system/redis.service
[Unit]
Description=The redis-server Process Manager
After=syslog.target network.target
[Service]
Type=forking
PIDFile=/var/run/redis_6379.pid
ExecStart=/usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf
ExecReload=/bin/kill -USR2 $MAINPID
ExecStop=/bin/kill -SIGINT $MAINPID
[Install]
WantedBy=multi-user.target
重新加载系统配置:systemctl daemon-reload
本次启动服务:systemctl start redis.service
开机自动启动reis服务:systemctl enable redis.service
1.2 Redis配置说明
#1、当客户端闲置多长时间后关闭连接,如果指定为 0,表示关闭该功能
timeout 300
#2、指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 verbose
loglevel notice
#3、日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null
logfile stdout
#4、设置数据库的数量,默认数据库为 0,可以使用 SELECT <dbid> 命令在连接上指定数据库 id
databases 16
#5、RDB数据同步间隔配置:指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
# RDB 文件是是经过压缩的二进制文件,占用空间很小,它保存了 Redis 某个时间点的数据集,很适合用于做备份。
# 如果想关闭RDB,则注释save 如果想删除之前的快照, 配置save '"。
# 如果同时开启了RDB和AOF下,会采用aof方式。AOF保存的数据方案时最完整的
save <seconds> <changes>
#Redis 默认配置文件中提供了三个条件:
#900秒内有1个更改时,进行同步
save 900 1
#300秒内有10个更改时,进行同步
save 300 10
#60秒内有10000个更改时,进行同步
save 60 10000
触发快照:1.save/bgsave命令 2.触发save point
#6、指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes
#7、指定本地数据库文件名,默认值为 dump.rdb
dbfilename dump.rdb
#8、指定本地数据库存放目录
dir ./
#9、设置当本机为 slave 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步
slaveof <masterip> <masterport>
#10、当 master 服务设置了密码保护时,slave 服务连接 master 的密码
masterauth <master-password>
#11、设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH <password>命令提供密码,默认关闭
requirepass foobared
#12、设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息
maxclients 128
#13、指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区
maxmemory <bytes>
14、指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no
appendonly no
#15、指定更新日志文件名,默认为 appendonly.aof
appendfilename appendonly.aof
#16、AOF指定更新日志条件,共有 3 个可选值:
#1) no:表示等操作系统进行数据缓存同步到磁盘(快)
#2) always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)
#3) everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec
#17、指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由 Redis 将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析 Redis 的 VM 机制)
vm-enabled no
#18、虚拟内存文件路径,默认值为 /tmp/redis.swap,不可多个 Redis 实例共享
vm-swap-file /tmp/redis.swap
#19、将所有大于 vm-max-memory 的数据存入虚拟内存,无论 vm-max-memory 设置多小,所有索引数据都是内存存储的( Redis 的索引数据就是 keys ),也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。默认值为 0
vm-max-memory 0
#20、Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享,vm-page-size 是要根据存储的数据大小来设定的,作者建议如果存储很多小对象,page 大小最好设置为 32 或者 64 bytes;如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值
vm-page-size 32
#21、设置 swap 文件中的 page 数量,由于页表(一种表示页面空闲或使用的 bitmap)是在放在内存中的,在磁盘上每 8 个 pages 将消耗 1byte 的内存。
vm-pages 134217728
#22、设置访问 swap 文件的线程数,最好不要超过机器的核数,如果设置为 0,那么所有对 swap 文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为 4
vm-max-threads 4
#23、设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
glueoutputbuf yes
#24、指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-zipmap-entries 64
hash-max-zipmap-value 512
#25、指定是否激活重置哈希,默认为开启(后面在介绍 Redis 的哈希算法时具体介绍)
activerehashing yes
#26、指定包含其它的配置文件,可以在同一主机上多个 Redis 实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
1.3 Redis使用
1.3.1 基础命令
基础命令(必须掌握)
进入客户端:/usr/local/redis/bin/redis-cli
设置key-value:set [key] [value]
查看所有key:keys *
模糊查找key:keys [xxx]*
判断某个key时候存在:exists 【key】
查看key的类型:type [key]
删除key:del 【key】
设置过期时间:expire [key] [seconds]
查看剩余过期时间(-2表示已经过期,-1表示永不过期):ttl [key]
取消过期时间:persist [key]
切换数据库(默认16个库,从0到15):select [0,1,2~15]
基础命令(拓展)
重命名key:rename [oldkey] [newkey]
清空当前数据库:flushdb
清空所有数据库:flushall
判断是否存在某个key:exists [key]
选择数据库(0-15,默认db0):select [dbIndex]
数据迁移到其他库:move [key] [dbIndex]
随机返回某个key:randomkey
数据库key数量:dbsize
查看数据库信息:info
看配置信息:config get *
1.3.2 常用数据类型
基础数据类型:String、Hash、List、Set、Zset
1) String数据类型
基础数据类型,[value]最多可以存512M的数据
设置数据(覆盖已有):set [key] [value]
设置数据(不覆盖):setnx [key] [value]
设置缓存时间(秒):setex [key] [seconds] [value]
获取数据:get [key]
删除数据:del [key]
替换字符串:setrange [key] [start] [newstring]
返回旧值设置新值:getset [key] [newvalue]
追加字符串:append [key] [endstring]
获取字符串长度:strlen [key]
批量设置数据:mset [key1] [value1] [key2] [value2] [key3] [value3]
批量获取数据:mget [key] [key] [key ]
自增自减:value是int数字
递增1:incr [key]
递减1:decr [key]
递增n:incrby [key] [n]
递减n:decrby [key] [n]
2) Hash数据类型
Hash:更适合存储对象类型,比String占用更少的空间,更方便获取整个对象
设置数据:hset [hashname] [fieldkey1] [value1]
设置数据(不覆盖):hsetnx [hashname] [fieldkey1] [value1]
获取数据:hget [hashname] [fieldkey]
批量设置数据:hmset [hashname] [fieldkey1] [value1] [fieldkey2] [value2]
批量获取数据:hmget [hashname] [fieldkey1] [fieldkey2]
删除所有属性:hdel [hashname]
设置缓存时间:hsetex [hashname]
返回hash长度:hlen [hashname]
返回hash所有key: hkeys [hashname]
返回hash所有value:hvals [hashname]
返回所有key-value:hgetall [hashname]
判断key是否存在:hexists [hashname] [field]
3) List数据类型
List:底层是一个双端链表结构,即可作为栈,也可作为队列
底层数据结构:快速链表
1)列表元素比较少的时候,使用连续内存保存数据,是ziplist即压缩列表
2)元素较多时候,将多个ziplist用双向指针组合成链表的方式存储数据,即quicklist
从头部添加元素(栈):lpush [listkey] [value1] [value2]
从尾部添加元素(队列):rpush [listkey] [value1] [value2]
取出所有数据:lrange [listkey] 0 -1
获取指点下标的元素:lindex [listkey] [index]
获取原始个数:llen [listkey]
插入元素:linsert [listkey] before [value1] [newvalue]
根据下标替换元素:lset [list] [index] [newvalue]
移除元素(移除n个值为value的元素):lrem [listkey] [num] [value]
从头部移除一个元素:lpop [listkey]
从尾部移除一个元素:rpop [listkey]
保留范围数据(其他都删掉):ltrlm [listkey] [startIndex] [endIndex]
4) Set数据类型
特点:数据无序、元素不重复、底层是hash表(元素的添加删除查找复杂度都是O(1) )
添加元素:sadd [setkey] [value1] [value2] [value3]
查看所有元素:smembers [setkey]
移除元素:srem [setkey] [value]
查看元素个数:scard [setkey]
判断集合是否包含某元素:sismember [setkey] [value]
随机返回一个元素:srandmember [setkey]
随机删除元素:spop [setkey] [count]
差集(获取set1中不在set2中的元素):sdiff [set1] [set2]
保存差集(获取set1中不在set2中的元素保存到set3中):sdiffstore [set3] [set1] [set2]
获取交集:sinter [setkey1] [setkey2]
保存交集:sinterstore [setkey1] [setkey2]
获取并集:sunion [setkey1] [setkey2]
保存并集:sunion [setkey3] [setkey1] [setkey2]
移动元素:smove [set1] [set2] [set1value]
5) Zset数据类型
Zset:数据有序,元素不能重复;可以用来做排行
数据结构:
1)使用hash,用于关联元素和score
2)使用跳跃表:能快速给元素排序,找到范围内的元素
添加元素(可覆盖):zadd [zsetkey] [core] [value]
查看所有元素(索引):zrange [zsetkey] 0 -1 withscores
根据序号查看指定范围数据:zrangebyscore [key] [core] [core]
删除指定元素:zrem [zsetkey] [value]
删除指定元素:zremrangebyscore [key] [core] [core]
获取正序索引:zrank [zsetkey] [value]
获取倒序索引:zrevrank [zsetkey] [value]
元素个数:zcard [zsetkey]
查询指定范围的元素个数:zcount [zsetkey] [core]
1.3.3 Redis6 中新的数据类型
1) Bitmaps 类型
说明:
说明:Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
1)Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
2)可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
使用场景:
例:统计系统每天访问的用户数量。用Bitmaps的偏移量作为用户id,数组每个单元值0表示未访问,1表示访问。
补充:档系统的用户访问量比较大的时候,这种数据类型来进行统计用户访问量是比较节省空间,如过系统访问量不多,用set保存数据也是比较节省空间的
命令:
setbit:设置Bitmaps中某个偏移量的值(0或1)
offset:偏移量从0开始(比如设置某用户某天是否访问过网站,将用户id设为偏移量,value置1表示访问过)
getbit:获取Bitmaps中某个偏移量的值,获取键的第offset位的值(从0开始算)
bitcount[start end] :统计字符串从start字节到end字节比特值为1的数量是字节不是位(统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含。)
bitop and(or/not/xor) [key…]:bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。(月活用这个比较方便)
2) HyperLogLog 类型
**使用场景:**解决需要去重计数的问题场景(简单方便,且节省空间)
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。
但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
1)数据存储在MySQL表中,使用distinct count计算不重复个数
2)使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog
说明:
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。(每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。)
因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
命令:
pfadd < element> [element …]:添加指定元素到 HyperLogLog 中将所有元素添加到指定HyperLogLog数据结构中。如果执行命令后key估计的近似基数发生变化,则返回1,否则返回0。
pfcount [key …] 计算key的近似基数,可以计算多个key,比如用key存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
pfmerge [sourcekey …] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
3) Geospatial 类型
说明
中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
指令
geoadd< longitude> [longitude latitude member…] 添加地理位置(经度,纬度,名称)
例:geoadd china:city 121.47 31.23 shanghai
两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。当坐标位置超出指定范围时,该命令将会返回一个错误。已经添加的数据,是无法再次往里面添加的。
geopos [member…] 获得指定地区的坐标值
geodist [m|km|ft|mi ] 获取两个位置之间的直线距离
单位:
m 表示单位为米[默认值]。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位
1.3.4 数据持久化
同时开启RDB和AOF时,数据恢复时Redis优先使用的是AOF的日志文件
1) RDB方式:
全称:Relational Database
说明:周期性的将内存中数据保存到硬盘中,默认dump.rdb文件(不适合实时系统使用)
配置说明:
Redis默认开启:
执行持久化:(默认配置如下)
save 900 1 900秒内写了1个key
save 900 10 300秒内写了10个key
save 900 10000 60秒内写了10000个key
优点:
数据恢复快,适合大规模数据恢复
节省磁盘空间
缺点:
1、因为fork线程会做数据同步(写时复制技术),故实际需要两倍的内存空间
2、在最后一次数据同步时,可能会存在丢失数据问题
2) AOF方式:
全称:Append Only File
说明:将写数据的操作保存到日志当中,默认appendonly.aof(日常工作中常用)
配置说明:
执行持久:(以下3选1)
appendfsync always 每次写操作进行(工作中)
appendfsync everysec 每秒进行(默认)
appendfsync no 不主动进行同步,把同步时机交给操作系统
异常恢复:
redis可以通过
redis-check-aof --fix appendonly.aof
对损坏的aof文件进行恢复
重写压缩操作:
Redis4.0之后的版本,fork进程会将rdb快照,以二进制的形式附在新的aof文件头部,作为已有历史数据,替换掉历史的流水账日志记录。
配置说明:
on-appendfsnc-on-rewrite yes 不写入aof文件只写入缓存,重写时用户请求不阻塞,但期间宕机会数据丢失
on-appendfsnc-on-rewrite no 仍然会数据持久化,当进行重写时,会阻塞
auto-aof-rewrite-percentage 100 设置重写基准版值:默认文件达到100%时才会重写(当前aof文件是上次重写后的文件的2倍时才是重写)
auto-aof-rewrite-min-size 64mb 设置重写的基准值,默认64mb(当前aof文件达到64mb时开始重写)
重写触发机制:
当前aof文件大小 >= 上次aof文件大小 * (1+100%[默认])
且
当前aof文件大小 >= 64mb[默认]
时,开始重写
首次重写:2 * 64mb[默认]
1.3.5 发布订阅
发布与订阅:
说明:Redis提供的发布与订阅是比较简单的,适用于即时通信(可主从同时使用)
订阅:subscribe [channel] [channe2]
退订:unsubscribe [channel] [channe2]
向指定频道发布:publish [channel] [message]
1.3.6 部署策略
1) 主从复制:
主从复制:(5.0之前称为maseter-slave;5.0开始称为maseter-replica
目的:实现读写分离,Master主要负责写入,slave负责读取
特点:
1、一个Master可以拥有多个slave;Master在同步数据时可以继续处理client请求,即不会阻塞
2、从服务器宕机后,主服务器仍然有数据变更,从服务器启动后,仍然会读到变更后的数据
过程:slave与Master建立链接发送同步命令,Master后台将缓存数据保存到文件中,最后将文件发送给salve
注意:
redis设置主从后,从服务器不能进行写操作
配置主从1:(Redis5.0之前)
说明:只需修改“从”服务配置
修改redis.conf:vim /usr/local/redis/etc/reids.conf
配置修改配置项:slaveof [masterIp] [maseterPort]
如果Master有密码,则需要修改配置项:masterauth [masterPassword]
配置主从2:(Redis5.0开始)
修改redis.conf:vim /usr/local/redis/etc/reids.conf
Master开放远程链接(注释掉其他bind):bind 0.0.0.0
配置修改配置项:replicaof [masterIp] [maseterPort]
配置从库是否只读(默认yes):replica-read-only
如果Master有密码,则需要修改配置项:masterauth [masterPassword]
指令:
查看主从信息(登录客户端发送):info replication
设置临时主机(登录客户端发送):slaveof [masterIp] [maseterPort]
2) 哨兵(切换主从):
目的:实现redis高可用
特点:进程独立,监控主从服务器状态,切换主从
过程:
配置:(在主从复制的配置基础上)
编辑sentinel.conf:vim /usr/local/redis/etc/sentinel.conf
设置为后台启动:
禁止保护模式:protected-mode no
监听主服务器:sentinel monitor [masterName] [maseterIP] [maseterPort] 2
(注:masterNam表自定义服务器的名称;“2”代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。)
定义服务密码(Master无密码时可不配置):sentinel auth-pass [masterName] [password]
启动哨兵进程:(所有主从Redis服务都启动后,再启动哨兵进程)
启动:/usr/local/redis/bin/redis-sentinel /usr/local/redis/etc/sentinel.conf
缺点:
- 主从切换的时候,会几秒间隔时间,可能会丢失数据
故障恢复:
主挂了后,在从中选择主。选择过陈如下
1、选择优先级最高的:根据redis配置的优先级,在redis.conf中存在replice-priority 100[默认100,值越小优先级越高]
2、选择偏移量对大的:从中的数据与主最一致的(同步时间离当前最短的)、
3、选择runid最小的:redis启动会随机生成40位的id
原主服务器启动后,将做为从服务器
3) 集群:
目的:实现redis高可用,扩容
特点:
1)机器更多,有多个主节点;
2)主节点保存数据,类似数据库的分片机制;
3)集群是一个整体,由redis自己处理数据存储到哪个节点,由redis自己处理(集群中每个主节分别占用有一个插槽范围,redis通过专门的算法计算key归属哪个插槽,进而确定数据存在哪个节点中);
4)去中心化,登录任何一台redis,都可以操作
环境要求:
实际集群需要6台以上服务器
需要安装ruby环境(redis6不在需要)
配置:
配置:(如过在同一台服务器模拟集群,则启动多个redis服务即可,注每个服务的端口要不同)
设置端口(单机模拟,端口要不同):port 6379
绑定当前机器ip:bind [ip]
指定数据文件存放位置:dir /usr/local/redis/etc
开启集群:cluster-enabled yes
指定集群配置文件: cluster-config-file nodes-6379.conf
节点失效认定时间:cluster-node-timeout 5000
打开AOF持久方式:appendonly yes
Redis6.0之前版本需要安装ruby环境:
yum install ruby
yum install rubygems
gem install redis
故障恢复:
主挂从未挂:
从变成主,原主启动后变成从(与哨兵中的一致)
主从都挂了:
根据cluster-require-full-coverage配置确定是否让集群都不可用
cluster-require-full-coverage yes 表示整个集群都挂掉
cluster-require-full-coverage no 整个集群不挂,挂掉的主所在范围的插槽不可使用,无法存储数据
优缺点:
优点:
实现扩容
分摊压力
配置简单
缺点:
多建操作不支持
多建的事务也不支持,lua脚本也不支持
技术出来比较晚,公司的老项目做迁移的话,工程量比较大
4) 单机模拟Redis集群:
至少需求启动6个reids
1.创建文件夹:
同台机器启动多个redis,需要多个配置配置文件,没个配置文件中配置的端口不能相同
mkdir -p /usr/local/redis-cluster/bin
mkdir -p /usr/local/redis-cluster/7001
mkdir -p /usr/local/redis-cluster/7002
mkdir -p /usr/local/redis-cluster/7003
mkdir -p /usr/local/redis-cluster/7004
mkdir -p /usr/local/redis-cluster/7005
mkdir -p /usr/local/redis-cluster/7006
2.拷贝文件:
cp /usr/local/redis-5.0.5/redis.conf /usr/local/redis-cluster/7001
cp /usr/local/redis-5.0.5/src/redis-server /usr/local/redis-cluster/bin
cp /usr/local/redis-5.0.5/src/redis-benchmark /usr/local/redis-cluster/bin
cp /usr/local/redis-5.0.5/src/redis-cli /usr/local/redis-cluster/bin
cp /usr/local/redis-5.0.5/src/redis-check-aof /usr/local/redis-cluster/bin
cp /usr/local/redis-5.0.5/src/redis-check-rdb /usr/local/redis-cluster/bin
cp /usr/local/redis-5.0.5/src/mkreleasehdr.sh /usr/local/redis-cluster/bin
cp /usr/local/redis-5.0.5/src/redis-sentinel /usr/local/redis-cluster/bin
配置文件也可以不拷贝,创建新的文件,文件头部使用
include [redis.conf文件路径]
引入公共配置,然后新配置文件中配置一些值
3.重命名文件:
mv /usr/local/redis-cluster/7001/redis.conf /usr/local/redis-cluster/7001/redis-7001.conf
4.修改配置:
vim /usr/local/redis-cluster/7001/redis-7001.conf
port 7001
bind 192.168.0.115
dir /usr/local/redis-cluster/7001/
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
appendonly yes
5.复制配置文件:
cp /usr/local/redis-cluster/7001/redis.conf /usr/local/redis-cluster/7002
cp /usr/local/redis-cluster/7001/redis.conf /usr/local/redis-cluster/7003
cp /usr/local/redis-cluster/7001/redis.conf /usr/local/redis-cluster/7004
cp /usr/local/redis-cluster/7001/redis.conf /usr/local/redis-cluster/7005
cp /usr/local/redis-cluster/7001/redis.conf /usr/local/redis-cluster/7006
7.修改配置文件名:
分别将7002、7003、7004、7005、7006下的redis-7001.conf,文件名以及配置中的7001改成相应的7002、70003等等
8.创建启动脚本:
- 切换目录:cd /usr/local/redis-cluster
- 创建脚本文件:touch start-redis-cluster.sh
- 编辑脚本:vim touch start-redis-cluster.sh
v#!/bin/sh
REDIS_HOME=/usr/local/redis-cluster
$REDIS_HOME/bin/redis-server $REDIS_HOME/7001/redis-7001.conf
$REDIS_HOME/bin/redis-server $REDIS_HOME/7002/redis-7002.conf
$REDIS_HOME/bin/redis-server $REDIS_HOME/7003/redis-7003.conf
$REDIS_HOME/bin/redis-server $REDIS_HOME/7004/redis-7004.conf
$REDIS_HOME/bin/redis-server $REDIS_HOME/7005/redis-7005.conf
$REDIS_HOME/bin/redis-server $REDIS_HOME/7006/redis-7006.conf
- 修改脚本文件为可执行文件:chmod 777 start-redis-cluster.sh
- 执行启动脚本:/usr/local/redis-cluster/start-redis-cluster.sh
9.创建集群:
只需创建一次,如过需要删除集群,需删除所有/usr/local/redis-cluster/700*/nodes-700*.conf
/usr/local/redis-cluster/bin/redis-cli --cluster create 192.168.0.115:7001 192.168.0.115:7002 192.168.0.115:7003 192.168.0.115:7004 192.168.0.115:7005 192.168.0.115:7006 --cluster-replicas 1
Redis6需要在src下执行命令
/usr/local/redis-6.0.9/src/redis-cli --cluster create --cluster-replicas 1 192.168.0.115:7001 192.168.0.115:7002 192.168.0.115:7003 192.168.0.115:7004 192.168.0.115:7005 192.168.0.115:7006
–cluster-replicas 1
cluster-replicas 1 表示每个主节点创建一个从节点
10.测试:
链接客户端(用任何一个节点连接都可以): /usr/local/redis-cluster/bin/redis-cli -c -h 192.168.0.115 -p 7001
查看集群所有节点:cluster nodes
设置单个值:set k1 v1
批量设置(需要对key进行分组):mset k1{[组名]} v1 k2{[组名]} v2
查看插槽某个key的所在的插槽值:cluster keyslot [key名]
查看某个插槽内有几个key(只能查看当前主机所在插槽的范围):cluster countkeysinslot [插槽值]
获取某个插槽中的所有key(只能查看当前主机所在插槽的范围):cluster getkeysinslot [插槽值]
11.Jedis操作集群
可以使用JedisCluster操作集群
还有其他方式,自行百度
二、Redis中事务和锁机制
2.1 Redis事务控制
2.1.1 Redis事务
Redis事物的本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行的中,会按照顺序执行
Redis事务流程:开启事务–》指令队列–》执行事务/取消事务
2.1.2 事务命令:Multi、Exec、Discard
命令说明:
Multi 开启事务
Exec 执行事务
Discard 取消事务
测试事务执行:
127.0.0.1:0>multi
"OK"
127.0.0.1:0>set k1 v1
"QUEUED"
127.0.0.1:0>set k2 v2
"QUEUED"
127.0.0.1:0>exec
1) "OK"
2) "OK"
测试取消事务:
127.0.0.1:0>multi
"OK"
127.0.0.1:0>set k3 v3
"QUEUED"
127.0.0.1:0>set k4 v4
"QUEUED"
127.0.0.1:0>discard
"OK"
127.0.0.1:0>
2.1.3 事务错误处理
组队过程中,若指令失败,则在事务执行时,会自动取消整个事务
127.0.0.1:0>multi
"OK"
127.0.0.1:0>set k1 v1
"QUEUED"
127.0.0.1:0>set k2
"ERR wrong number of arguments for 'set' command"
127.0.0.1:0>set k3 v3
"QUEUED"
127.0.0.1:0>exec
"EXECABORT Transaction discarded because of previous errors."
127.0.0.1:0>
命令组队时,没有失败,执行时某条指令失败,此时会跳过执行失败的命令,继续执行队列中的下一条指令
如下测试:第二条指令对非数字的value进行加1操作
127.0.0.1:0>multi
"OK"
127.0.0.1:0>set k1 v1
"QUEUED"
127.0.0.1:0>incr k1
"QUEUED"
127.0.0.1:0>set k2 v2
"QUEUED"
127.0.0.1:0>exec
1) "OK"
2) "ERR value is not an integer or out of range"
3) "OK"
127.0.0.1:0>
2.2 Redis锁机制
2.2.1 悲观锁
每次取数据时,都认为别人会修改数据,故每次取数据时都对数据加锁,即操作公共资源之前进行加锁。(传统关系型数据库用的就是这种机制)
2.2.2 乐观锁
每次读取数据时,认为别人不会修改数据,故此时不加锁,但是需要修改数据需要判断别人有没有修改数据,可以使用版本号来进行判断。
适用:乐观锁适用于多读的应用,这样可以提高系统的吞吐量。(Redis中就是使用这种)
2.2.3 乐观锁带来的问题:
问题你描述:库存遗留问题
在高并发情况下,如果两千个人同时获取到V1.0版本的数据,然后同时提交的话,那么最终将只会有一个人修改成功,其余的1999人都会修改失败。
解决方案:
Redis 2.6以上的版本,可以通过库存遗留问题可以通过。
LUA脚本就是将复杂的或者多步的redis操作,写成一个脚本,一次提交给Redis执行,减少反复连接redis的次数。提升性能。
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
**解决秒杀案例中的:**库存遗留问题Lua脚本示例
local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local userskey="sk:"..prodid..":user";
local userExists=redis.call("sismember",userskey,userid);
if tonumber(userExists)==1 then
return 2;
end
local num=redis.call("get",qtkey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",qtkey);
redis.call("sadd",userskey,userid);
end
return 1
**测试类:**使用jedis.scriptLoad()方法调用lua脚本
@Service
public class SeckillByScript {
static String secKillScript1 = "local userid=KEYS[1];\n" +
"local prodid=KEYS[2];\n" +
"local qtkey=\"sk:\"..prodid..\":qt\";\n" +
"local userskey=\"sk:\"..prodid..\":user\";\n" +
"local userExists=redis.call(\"sismember\",userskey,userid);" +
"if tonumber(userExists)==1 then\n" +
" return 2;\n" +
"end\n" +
"local num=redis.call(\"get\",qtkey);\n" +
"if tonumber(num)<=0 then\n" +
" return 0;\n" +
"else\n" +
" redis.call(\"decr\",qtkey);\n" +
" redis.call(\"sadd\",userskey,userid);\n" +
" end\n" +
" return 1";
public boolean doSecKill(String userid, String prodid) {
JedisPool jedisPool = JedisPoolUtil.getJedisPool();
Jedis jedis = jedisPool.getResource();
String sha1 = jedis.scriptLoad(secKillScript1);
Object result = jedis.evalsha(sha1, 2, userid, prodid);
String reString = String.valueOf(result);
if ("0".equals(reString)) {
System.out.println("已抢空!");
return false;
} else if ("1".equals(reString)) {
System.out.println("抢购成功");
return true;
} else if ("2".equals(reString)) {
System.out.println("该用户已抢过!");
return false;
} else {
System.out.println("抢购异常");
return false;
}
}
}
2.2.4 watch指令
指令:watch
在执行multi之前先执行watch [key1 key2 …] 监视一个或多个key。如果事务在执行之前,被监视的key的value被其他程序修了,则当前事务被打断。
测试:
- 准备数据:
127.0.0.1:6379> set k1 1000
OK
- 客户端1:使用watch监控,并创建事务准备修改数据,事务先不执行
127.0.0.1:6379> watch k1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1
QUEUED
- 客户端2:使用watch监控,并进行数据修改(可以修改成功)
127.0.0.1:0>watch k1
"OK"
127.0.0.1:0>incr k1
"1001"
127.0.0.1:0>
- 客户端1:执行事务(事务终止,队列中的所有指令都不执行)
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>
指令:unwatch
unwatch:取消watch对所有key的监视。
注:在执行watch最后,事务执行了exce或discard,那么不需要执行unwach
3、事务的特性
隔离性
事务中的指令都按顺序执行,执行过程中不会被其他客户端发来的指定打断
没有隔离级别
队列中的指令在事务提交前都不会被实际执行
不保障原子性
事务执行过程中有一条指令执行失败,后续指令仍然会继续执行,不会进行回滚
三、Spring整合Redis
3.1 概述
Jedis是Redis官方推荐的Java连接开发工具。要在Java开发中使用好Redis中间件,必须对Jedis熟悉才能写成漂亮的代码。
在 springboot 1.5.x版本的默认的Redis客户端是 Jedis实现的,springboot 2.x版本中默认客户端是用 lettuce实现的
Jedis与Lettuce
说明:
Lettuce 和 Jedis 的定位都是Redis的client,所以他们当然可以直接连接redis server
Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接
Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
3.2 Springboot2.0通过Lettuce整合Redis
因为 springboot2.0中默认是使用 Lettuce来集成Redis服务,spring-boot-starter-data-redis默认只引入了 Lettuce包,并没有引入 jedis包支持。所以在我们需要手动引入 jedis的包,并排除掉 lettuce的包,pom.xml配置如下:
<!--springboot整合redis,springboot2.0默认使用Lettuce-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis需要使用连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
#redis主机地址
spring.redis.host=127.0.0.1
#redis连接端口
spring.redis.port=6379
#redis密码
spring.redis.password=123456
#redis数据库索引
spring.redis.database=0
#redis连接超时时间
spring.redis.timeout=3000s
#redis最大连接数
spring.redis.lettuce.pool.max-active=100
#最多维持空闲连接
spring.redis.lettuce.pool.max-idle=10
#最少维持空闲连接
spring.redis.lettuce.pool.min-idle=10
#连接池出借连接的最长期限
spring.redis.lettuce.pool.max-wait=30000s
配置类:
@Configuration
public class RedisConfig {
/**
* 自定义RedisTemplate
* <p>
* 默认情况下的模板只能支持 RedisTemplate<String,String>,只能存入字符串,
* 很多时候需要自定义 RedisTemplate ,设置序列化器,这样我们可以很方便的操作实例对象
*
* @param connectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
实体类:
@Data
public class User implements Serializable {
private static final long serialVersionUID = -1622274450182629030L;
private Integer id;
private String username;
private Integer age;
public User() {
}
public User(Integer id, String username, Integer age) {
this.id = id;
this.username = username;
this.age = age;
}
}
测试:
@SpringBootTest
@Slf4j
class Redis01ApplicationTests {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@Test
public void testRedis() {
String key = "user:1";
redisTemplate.opsForValue().set(key, new User(1, "pjmike", 20));
User user = (User) redisTemplate.opsForValue().get(key);
log.info("uesr: " + user.toString());
}
}
3.3 SpringBoot2.0通过Jedis整合Redis
因为 springboot2.0中默认是使用 Lettuce来集成Redis服务,spring-boot-starter-data-redis默认只引入了 Lettuce包,并没有引入 jedis包支持。所以在我们需要手动引入 jedis的包,并排除掉 lettuce的包
springboot和jedis版本需要对应才行。
springboot2.1.3.RELEASE 使用 jedis2.9.0
springboot2.2.0.RELEASE 使用 jedis3.1.0
springboot2.3.3.RELEASE 使用 jedis3.1.0
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--SpringBoot2.0使用Jedis,排除lettuce包-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--SpringBoot2.0使用Jedis,引入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- spring2.X集成redis所需common-pool2,使用jedis必须依赖它 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
#redis连接端口
spring.redis.port=6379
#redis密码
spring.redis.password=123456
#redis数据库索引
spring.redis.database=0
#redis连接超时时间
spring.redis.timeout=3000s
#redis最大连接数
spring.redis.jedis.pool.max-active=100
#最多维持空闲连接
spring.redis.jedis.pool.max-idle=10
#最少维持空闲连接
spring.redis.jedis.pool.min-idle=10
#连接池出借连接的最长期限
spring.redis.jedis.pool.max-wait=30000s
配置类型:
/**
* Redis配置类
*
* @AutoConfigureAfter(RedisAutoConfiguration.class) 是让我们这个配置类在内置的配置类之后在配置,这样就保证我们的配置类生效,并且不会被覆盖配置
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
/**
* 配置自定义redisTemplate
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//1:使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setDefaultSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
template.setValueSerializer(jackson2JsonRedisSerializer);
//2:使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(RedisSerializer.string());
// template.setValueSerializer(RedisSerializer.string());
// template.setHashKeySerializer(RedisSerializer.string());
// template.setHashValueSerializer(RedisSerializer.string());
// 配置共享本地连接设置
// template.setShareNativeConnection(false);
return template;
}
}
实体类:
@Data
public class User implements Serializable {
private static final long serialVersionUID = -1622274450182629030L;
private Integer id;
private String username;
private Integer age;
public User() {
}
public User(Integer id, String username, Integer age) {
this.id = id;
this.username = username;
this.age = age;
}
}
测试:
@SpringBootTest
@Slf4j
class Reids02ApplicationTests {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
public void testRedis() {
String key = "user:2";
redisTemplate.opsForValue().set(key, new User(1, "atest", 20));
User user = (User) redisTemplate.opsForValue().get(key);
log.info("uesr: " + user.toString());
}
@Test
public void testRedisTemplate() {
final String key = "obj";
List<String> strs = Arrays.asList("d", "b", "a", "c", "a");
ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, strs);
Object str = opsForValue.get(key);
System.out.println(str);
}
}
3.3 基本使用(人工创建连接池)
依赖:
<!--SpringBoot2.0使用Jedis,引入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- spring2.X集成redis所需common-pool2,使用jedis必须依赖它 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
配置类:
@Configuration
public class JedisPoolFactory {
/**
* 自动注入redis配置属性文件
*/
@Autowired
private RedisProperties properties;
@Bean
public JedisPool getJedisPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(properties.getJedis().getPool().getMaxIdle());
config.setMaxTotal(properties.getJedis().getPool().getMaxActive());
config.setMaxWaitMillis(properties.getJedis().getPool().getMaxWait().toMillis());
JedisPool pool = new JedisPool(config, properties.getHost(), properties.getPort(), 100, "123456");
return pool;
}
}
测试:
@SpringBootTest
@Slf4j
class Reids02ApplicationTests {
@Resource
private JedisPool jedisPool;
@Test
public void testGetJedis() {
Jedis jedis = jedisPool.getResource();
jedis.set("jedisKey", "testVlue");
System.out.println(jedis.keys("*"));
}
}
代码中使用:
Jedis调用方法,其实与Redis的命令息息相关。例如keys *** 命令可以用jedis.keys(“*”)**;
@Test
void contextLoads() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
//设置密码
jedis.auth("123456");
//基本使用
jedis.keys("*");
jedis.set("k1", "v1");
Set<String> k1 = jedis.keys("k1");
System.out.println(k1);
String k11 = jedis.get("k1");
System.out.println(k11);
//关闭连接
jedis.close();
}
四、Redis6新特性:
4.1 ACL
描述:
redis6之后,提供了更细粒度的权限控制
1、接入权限:用户名和密码
2、控制可以执行的命令
3、控制可以操作的key
指令示例:
#查看权限列表
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
# default表示用户名,on表示是否启用,nopass表示没有密码,~*表示可操作性的key, +@all表示可执行的命令
#增加用户权限
127.0.0.1:6379> acl setuser lilei
OK
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user lilei off &* -@all"
#查看当前操作用户
127.0.0.1:6379> acl whoami
"default"
#设置一个密码是123456 只能够使用get的命令的用户,
#并且在key前一定要加上`cached:`这个限制
127.0.0.1:6379> acl setuser lilei1 on >123456 ~cached:* +get
OK
#检查是否设置成功
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user lilei off &* -@all"
3) "user lilei1 on #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 ~cached:* &* -@all +get"
#切换用户操作
127.0.0.1:6379> auth lilei1 123456
OK
#没有权限的key,无法操作
127.0.0.1:6379> get a
(error) NOPERM this user has no permissions to access one of the keys used as arguments
#有权限的key可以正常操作
127.0.0.1:6379> get cached:a
(nil)
4.2 IO多线程
描述:
6.0之后增加了多线程的实现,多线程使用在io的操作上,指令的执行还是只有一个单线程,多的io线程用于读read或者写write,
多线程不会同时执行读写操作。所有多出的线程要不是全部用于读,要不全部都是用于写。
#6.0多线程的配置默认情况下是关闭的,需要通过配置开启。开启多线程后,还需要设置线程数,否则是不生效的。同样修改redis.conf配置文件
io-threads 4 #默认开启4个线程,其中包含1个main线程和3个写线程,默认情况下不开启读线程
io-threads-do-reads no#默认情况下,开启了多线程之后是用来写操作的,
如果此设置为yes则表示新开启的线程用于读操作
官方建议:
4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。官方认为超过了8个基本就没什么意义了
Redis6.0之前的版本真的是单线程吗?
所谓的单线程:Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回(socket写) 等都由一个顺序串行的主线程处理。
存在其他辅助线程:Redis4.0之后,除了主线程外,还有其他辅助线程在处理一些较为缓慢的操作,例如数据持久化、清理脏数据、无用连接的释放、大key的删除等等。
Redis6.0之前为什么一直不使用多线程?
1、Redis几乎不存在CPU成为瓶颈的情况,Redis主要受限于内存和网络。(普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求)
2、Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程
3、单线程机制使得 Redis 内部实现的复杂度大大降低
Redis6.0为什么要引入多线程呢?
80%公司实际不需要,主要是针对QPS动不动就上亿项目,常需要搭建redis集群,集群需要数据同步,此时redis瓶颈就体现IO上,引入IO多线程能分摊IO负荷
五、Redis常见问题
5.1 经典问题
5.1.1 缓存穿透
现象:
应用服务压力增大,redis状态正常但是命中率降低,数据库中查询不到数据
分析:
redis查询不到数据,出现很多非常常访问,可能受到恶意攻击
解决方案:
- 对空值做缓存:对查询不到的数据做缓存,但是缓存时间不以过长(临时处理方案)
- 设置白名单:可以利用redis的bitmaps数据类型,设置可以访问的用户(缺点每次请求都需要判断)
- 采用布隆过滤:原理与bitmaps类似,即判断数据存不存在
- 实时监控:发现redis命中率急速降低时,对系统的访问用户进行排查
5.1.2 缓存击穿
现象:
数据库压力瞬间增大;redis中没有大量的key过期;redis正常运行
分析:
redis中某个key过期了,系统大量访问该key,在再次缓存改key之前,有大量请求直接查询数据库
解决案:
设置热门数据:将系统的热门数据进行缓存,并适当延长过期时间
实时监控:实时监控redis看哪些是热门数据,针对热门数据延长过期时间
使用双重检测锁:实现高并发情况下,只查询一次数据库,查询数据后立马缓存
5.1.3 缓存雪崩
现象:
数据压力增大,导致系统崩溃
分析:
某一时刻大片缓存数据因超时而被清理,此时大量查询数据库
解决方案:
**构建多级缓存:**采用不同层级多处缓存;(机构相对复杂)
**使用锁或者队列:**锁能降低数据库访问压力,队列有削峰的作用
**设置过期标识更新缓存:**设置标识,监控快过期时,进行数据刷新
**分散过期时间:**根据业务将相应的数据的缓存时间分开设置,不要集中在某一时刻
5.1.4 Key的过期策略:
Redis的Key有3种过期删除策略,具体如下:
- 定时删除
**原理:**在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作
**优点:**能够很及时的删除过期的Key,能够最大限度的节约内存
**缺点:**对CPU时间不友好,如果过期的Key比较多时,可能会占用相当一部分CPU时间,对服务器的响应时间和吞吐量造成影响 - 惰性删除
原理:在取出键时才对键进行过期检查,如果发现过期了就会被删除
**优点:**对CPU友好,能够最大限度的节约CPU时间
**缺点:**对内存不友好,过期的Key会占用内存,造成浪费 - 定期删除
**原理:**定期删除策略是定时删除策略和惰性删除策略的一个折中。定期删除策略每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
**优点:**对CPU时间和内存空间的一种权衡,可以根据实际使用情况来调整删除操作执行的时长和频率
**缺点:**确定删除操作执行的时长和频率很难。如果删除操作执行的太频繁,或者执行的时间太长,退化成定时删除策略;如果删除操作执行的太少,或者执行时间太短,退化成惰性删除策略 - **Redis服务器实际使用的是惰性删除和定期删除两种策略:**通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。Redis默认每隔100ms随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除。
5.1.5 内存淘汰机制
如果Redis服务器打开了maxmemory选项,并且服务器占用的内存数超过了maxmemory选项所设置的上限值时,会进行内存淘汰,常见的淘汰策略如下:
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-lfu:从数据集(server.db[i].dict)中挑选使用频率最低的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
5.1.5 分布式锁
目标:
针对分布系统,所有机器都能公用的锁即分布式锁
常见实现方案:
基于数据库实现
基于缓存实现(Redis)
基于Zookeeper
Redis实现分布式锁优缺点:
优点:
实现原理简单,性能高
缺点:
不是100%可靠:如果在redis集群中使用分布式锁,主从复制存在延时,这样的延时不能保证锁的安全性。
1)redis实现分布式锁
- 使用setnx
setnx key value 表示不覆盖设置key-value,即原有key已经存在,即设置值不生效(这本身就是锁的意思)
上锁:setnx [key] [value]
手动释放锁:del [key]
手动防止锁未释放(设置过期时间):expire [key] [second]
缺点:使用setnx设置后,如果没有及时设置过期时间,还是存在不释放锁的风险
- 使用set
set [key] [value] nx ex [second] :使用set设置数据的同时,进行上锁,且设置过期时间
上锁:set [key] [value] nx ex [second]
释放锁:del [key]
2)UUID防止误删
描述:
在程序的末尾手动释放锁之前,发生了阻塞,且阻塞时间正好超过了key的过期时间,那么其他线程就会获得锁,此时被阻塞的线程手动释放锁时,可能释放是别人的。
解决方式:
使用UUID标识:对上锁的资源,的值用唯一的数据进行标识(例如UUID),程序手动释放锁时,对值做比较。
@PostMapping("/testLock")
public void testLock() {
String yyds = UUID.randomUUID().toString();
/*获取锁*/
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", yyds, 3, TimeUnit.SECONDS);
/*获取锁成功*/
if (lock) {
Object num = redisTemplate.opsForValue().get("num");
if (StringUtils.isEmpty(num)) {
return;
}
int tic = Integer.parseInt(num + "");
redisTemplate.opsForValue().set("num", ++tic);
/*获取锁的uuid,只能释放自己的锁*/
String uuidLock = (String)redisTemplate.opsForValue().get("lock");
if(yyds.equals(uuidLock)){
/* 释放锁,del*/
redisTemplate.delete("lock");
}
} else {
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3)Lua脚本保证删除原子性
描述:
使用UUID做防止误删除时,假如Java判断UUID相等后程序发生了阻塞,key正好过期,其他线程获得锁,此时被阻塞线程手动释放锁,可能释放的是别的人的。
解决方式:
使用lua脚本来判断uuid,且根据判断结果做手动师傅锁的操作,这样就能保证手动释放锁的原子性(判断+删除不会被打断)
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String locKey = "lock";
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
Thread.sleep(1000);
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5.2 常见面试题
5.2.1 Redis与Memcache对比
1.支持的数据类型不同
memcache支持string类型,图片,视屏等缓存;
redis不仅支持简单的k/v类型,还提供list,hash,set,zset等数据类型。
2.是否支持持久化
memcache不支持持久化,数据都是在内存中;
redis支持持久化,通过RDB/AOF两种方式来将数据持久化到磁盘。
3.内存空间和key数量
memcache的key最长250个字符,value不能超过1MB(可以通过配置文件修改);
redis的key和valve均不能超过512MB。
4.缓存时间
memcache默认支持最多缓存30天;
redis缓存时间没有限制。
5.应用场景不同
redis不仅用做数据缓存,还可以用来消息队列,数据堆栈等方面;
memcache适合简单数据类型,热点常用数据。
6.redis支持主从复制
5.2.2 海量数据判断key是否存在?
使用布隆过滤器
使用Guava包种的布隆过滤器
5.2.3 业务中使用用Hash还是用String保存数据对象?
如果业务频繁的只取某对象的某几个字段,使用Hash要好一点;
如果业务中频繁的使用整个对象的数据,那么用String类型即key+json较为方便
(大量取数据的时候区别才会明显)
5.2.4 Stirng类型怎么存储的?
5.2.5 Redis是怎么保证大量请求的?
5.2.6 有哪些策略原理是啥?
5.2.7 redis为啥默认为16个数据库?
1,早期版本没有集群,为了的安全而这样设计(分片备份数据)
5.2.8 持久化方式有哪些?
方式一(默认方式):RDB,周期性的将内存中数据保存到硬盘中,默认dump.rdb文件
方式二:AOF,将写数据的操作保存到日志当中,默认appendonly.aof
5.2.9 生产环境中的 redis 是怎么部署的?
连环问题:
- 你的 redis 是主从架构?集群架构?用了哪种集群方案?
- 有没有做高可用保证?
- 有没有开启持久化机制确保可以进行数据恢复?
- 线上 redis 给几个 G 的内存?
- 设置了哪些参数?
- 压测后你们 redis 集群承载多少 QPS?
面试官心理分析:
看看你了解不了解你们公司的 redis 生产集群的部署架构,如果你不了解,那么确实你就很失职了。这些你必须是门儿清的,否则你确实是没好好思考过。
答案示例:
redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。5 台机器对外提供读写,一共有 50g 内存。因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。其实大型的公司,会有基础架构的 team 负责缓存集群的运维。