目录
视频地址:https://www.bilibili.com/video/BV1S54y1R7SB?p=1
NoSQL概述
Redis入门
概述
Windows安装
1、下载地址:https://github.com/tporadowski/redis/releases
2、解压安装包
下载到本地后,进行解压,得到目录如下图
3、启动Redis服务
在解压目录下双击redis-server.exe
,或打开命令行,执行下列命令,即可启动redis服务
redis-server.exe redis.windows.conf
4、使用Redis客户端连接Redis
在解压目录下双击redis-cli.exe
,或打开命令行,执行下列命令,即可连接redis服务
redis-cli.exe -h 127.0.0.1 -p 6379
Windows下使用确实简单,但是官方推荐使用Linux部署Redis服务!
Linux安装
1、下载地址:http://redis.io/download
2、解压Redis安装包
3、进入解压后的文件,可以看到Redis的配置文件
4、安装Redis
在解压后的文件夹内执行下列命令
yum install gcc-c++
make MALLOC=libc
make install
执行make
命令后,在src目录下,会生成redis-server、redis-cli等可执行命令文件
5、默认安装路径
在执行make install
命令后,在/usr/local/bin/
目录下,也会有redis-server、redis-cli等可执行命令文件
6、修改Redis配置文件
默认不是后台启动,改为后台启动模式
7、启动Redis服务
src/redis-server redis.conf
8、使用redis-cli进行连接
redis-cli -h ip地址 -p 端口号 -a 密码
9、关闭redis服务
使用redis-cli连接上redis服务后,使用shutdown命令关闭redis服务
测试性能
redis-benchmark是一个官方自带的压力测试工具。
redis-benchmark [option] [option value]
# 测试:100个并发连接,10W个请求
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000
基础的知识
1、redis默认有16个数据库,在redis.config
配置文件中有
默认使用的是第0个。
可以使用select
命令切换数据库,可以使用dbsize
命令查看当前数据库的大小
[root@localhost redis-6.2.4]# redis-cli
127.0.0.1:6379> select 3 # 切换数据库
OK
127.0.0.1:6379[3]> dbsize # 查看db大小
(integer) 1
2、查看当前数据库所有的key,用keys *
命令
127.0.0.1:6379[3]> keys * # 查看当前数据库中所有的key
1) "name1"
2) "name"
3、可以使用flushall
命令清空所有数据库的数据,使用flushdb
清空当前数据库的数据
127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> keys *
(empty array)
4、Redis是单线程的
Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis的性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程实现,就直接使用单线程了!
Redis是C语言写的,官方提供的数据为10W+的QPS,完全不比同样使用key-value的Memcache差!
Redis为什么单线程还这么快?
a)误区1:高性能的服务器一定是多线程的?
b)误区2:多线程(CPU上下文会切换!)一定比单线程效率高!
核心:Redis是将所有的数据全部放在内存中的,所以说使用单线程操作效率就是最高的,对于内存来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳方案
五大数据类型
官方文档
全段翻译:
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件MQ。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
Redis-Key
Redis常用命令
KEYS * # 查看当前数据库所有的key
SET name kuangshen # 设置key为name的值是kuangshen
MOVE name 1 # 移除1号数据库的key为name的数据
EXISTS name # 判断key为name的数据是否存在,如果存在返回1,不存在返回0
EXPIRE name 10 # 设置key为name的数据10秒后过期,过期的数据会自动删除
TTL name # 查看key为name的字段还有多久过期,返回值为剩余秒数,如果返回-1表示永不过期,返回-2表示已过期
TYPE name # 查看key为name的数据的数据类型,对应于下面的五种数据类型
后面如果遇到不会的命令,可以在官网查看帮助文档!
Redis中的key是大小写敏感的,即name和NAME是两个不同的key!
String(字符串)
String类型相关的常用命令
# 字符串操作
APPEND key1 hello # 往key为key1的字符串后面追加"hello"字符串,返回的是追加后的字符串长度,如果其输入的key不存在,则直接创建,然后在空字符串后面追加输入的字符串
STRLEN key1 # 获取key为key1的字符串的长度
GETRANGE key1 0 3 # 获取key为key1对应的字符串的第0位到第3位(共四个字符),Redis中,字符串下标从0开始,相当于Java中String的substr()函数
GETRANGE key1 0 -1 # 获取key为key1对应的字符串的第0到第-1位,即获取整个字符串
SETRANGE key2 1 xx # 从第1位(字符串下标从0开始)替换key为key2对应的字符串的内容为xx,若key2原来的值是abcdefg,替换后则为axxdefg,返回值为替换后的字符串内容,相当于Java中String的replace()函数
SETEX key3 30 hello # 设置key为key3的值是hello,并且在30秒后过期,相当于依次执行SET key3 hello和EXPIRE key3 30两个命令,其含义是set with expire
SETNX mykey redis # 如果key为mykey的值不存在,则创建mykey的值为redis(返回1),如果存在,则创建失败(返回0),其含义是set if not exists,在分布式锁中会常常使用
# int操作
INCR views # 将key为views对应值自增加一(相当于i++),返回值是自增后的结果,如果key不存在,则创建key(其默认值为0),然后自增加一(即结果为1),如果自增的key对应的值不是int型,则会报错,原子操作
DECR views # 将key为views对应值自减减一(相当于i--),其他都和INCR命令一样,原子操作
INCRBY views 10 # 将key为views对应的值自增加10(相当于 i += 10),原子操作
DECRBY views 10 # 将key为views对应的值自增减10(相当于 i -= 10),原子操作
# 批量操作
MSET k1 v1 k2 v2 k3 v3 # 批量执行set操作,是原子操作,设置k1的值为v1,k2的值为v2,k3的值为v3,使用空格分隔,键值对一一对应
MGET k1 k2 k3 # 批量获取值,批量执行get操作,用空格间隔要获取的key,返回值是指定的key对应的values的list
MSETNX k1 v1 k4 v4 # 批量执行setnx操作,是原子操作,如果k1和k4都不存在,则创建k1的值为v1,k4的值为v4,如果k1存在,k4不存在,也会创建失败的,创建成功返回1,创建失败返回0
# 对象操作
SET user:1 {name:zhangsan,age:3} # 设置key为user:1的值为一个json字符串,如果值没有用双引号括起来,则不能有空格,否则就必须用双引号括起来
# 除了上述方法外,如果想用redis存储一个对象,还可以通过key的设计换一种方式存储,即key改为user:{id}:{filed}
mset user:1:name zhangsan user:1:age 3
# 获取时使用mget命令批量获取
mget user:1:name user:1:age
# 组合命令
getset db redis # 先获取key为db的值,然后再将db的值设置为redis,所以该命令的返回值是当前key为db的值(如果不存在值,则返回nil),当执行完这个命令后,key为db的值变为了redis
String类型的应用场景:value除了是字符串,还可以是数字。
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
List(列表)
在Redis里面,可以把List当成栈、队列、阻塞队列使用!
List相关的命令大多是以L开头的,少部分是以R开头的。
# 从队列的左边(头部)入队一个或多个元素
lpush list one # 从list队列的左边(头部)入队一个元素one
lpush list two # 从list队列的左边(头部)入队一个元素two
lpush list three # 从list队列的左边(头部)入队一个元素three
# 查看队列的内容
lrange list 0 -1 # 查看list队列的从左到右的所有元素,0代表从0开始,-1代表到第-1个元素截止,即所有元素,返回值是list列表的全部元素,结合上面的lpush命令的情况,这里返回的应该是three、two、one
lrange list 0 1 # 查看list队列从左到右的第0和第1个元素,结合上面的lpush命令的情况,这里返回的应该是three、two
# 从队列的右边(尾部)入队一个或多个元素
rpush list right # 从list队列的右边(尾部)入队一个元素right,结合上面的lpush命令的情况,此命令执行之后,队列的内容应该是three、two、one、right
# 从队列中弹出一个元素
lpop list # 从list队列的左边(头部)弹出一个元素,返回值为弹出的元素内容,结合上面的命令,返回值为three
rpop list # 从list队列的右边(尾部)弹出一个元素,返回值为弹出的元素内容,结合上面的命令,返回值为right
#通过索引获取元素的值
lindex list 0 # 获取list列表的第0个元素的值
lindex list 1 # 获取list列表的第1个元素的值
# 获取列表的长度
llen list # 获取list列表的长度
# 从列表中移除指定的值
lrem list 1 one # 从list列表中移除1个one,是精确匹配,返回值是移除掉的元素的个数
lrem list 2 three # 从list列表中移除2个three,如果此时list中有2个及以上的three,这里返回2,如果只有1个three,这里返回1
# 截取列表指定范围的值,剩余的值将被删除
ltrim myList 1 2 # 截取myList列表的值,从下标1开始,到下标2结束(列表的下标从0开始),如果myList列表的原值为[hello, hello1, hello2, hello3],那截取过后myList列表的新值为[hello1, hello2]
# 从A列表移除最后一个(表尾)元素,并添加进B列表表头
rpoplpush myList myOtherList # 从myList列表中移除其最右边一个元素,并添加进myOtherList列表的最左侧,返回值为被移动的元素,如果myList的原值是[hello, hello1, hello2],则返回值是hello2,移动后myList的新值是[hello, hello1],myOtherList的新值是[hello2, ...]
# 更新列表中指定下标的值
lset list 0 item # 将list列表的第0位的值更新成item,这个命令有两个前提条件,一是list列表必须存在,二是指定的下标(这里是第0位)必须存在,否则将报错
# 向列表中指定元素的前面或者后面插入一个值
linsert myList before world other # 向myList列表的world元素前插入other元素,若myList列表的原值是[hello, world],执行命令后myList列表的新值是[hello, other, world]
linsert myList after world ! # 向myList列表的world元素后面插入!元素,若myList列表的原值是[hello, other, world],执行命令后myList列表的新值是[hello, other, world, !]
小结:
- Redis的List实际上是一个链表,链表由node组成,所以可以在node的前面或者后面做插入操作
- 在执行push操作时,如果key不存在,则会创建新的链表,如果key存在,则新增内容
- 如果移除了所有的值,即空链表,则List也不存在了
- 在List两边插入或改动值的时候,效率最高!但是操作中间元素的话,效率会低一点
使用场景:
- 消息队列(lpush rpop):从左边入队,然后右边出队
- 栈(lpush lpop):从左边入队,然后左边出队
Set(集合)
set中的值是不能重复的。
set相关的命令都是以S开头的。
set是无序不重复集合。
# 向集合中添加元素
sadd myset hello kuangshen "love kuangshen" # 向myset集合中添加hello,kuangshen和love kuangshen三个元素,如果添加的元素中包含空格,就需要使用双引号括起来
# 查看集合的所有元素
smembers myset # 查看myset集合中的所有元素
# 判断某个值是否在集合中
sismember myset hello # 判断hello元素是否在myset集合中,精确匹配,如果在则返回1,否则返回0
# 查看某个集合的元素个数
scard myset # 查看myset集合的元素个数
# 移除集合中的某个元素
srem myset hello # 将hello从myset集合中移除
spop myset # 随机从myset集合中移除2个元素,返回值为被移除的元素
spop myset 2 # 随机从myset集合中移除2个元素,返回值为被移除的元素
# 随机筛选集合中的指定个数的元素
srandmember myset # 从myset集合的值中随机筛选出1个,返回值是筛选出来的元素
srandmember myset 2 # 从myset集合的值中随机筛选出2个
# 将一个指定的元素移动到另外一个集合中
smove myset myset2 kuangshen # 将kuangshen元素从myset集合中,移动到myset2集合中,若移动成功返回值为1,若移动失败返回值为0
# 两个集合进行比较
sdiff key1 key2 # 找出在key1集合中有,但是key2集合中没有的元素,返回值为元素列表,该命令可以同时比较更多集合
sdiff key1 key2 key3 # 找出在key1集合中有,但是key2和key3集合中没有的元素
sinter key1 key2 # 找出在key1集合和key2集合中都存在的元素(并集),该命令可以同时比较更多集合
sinter key1 key2 key3 # 找出在key1、key2、key3集合中都存在的元素(并集)
sunion key1 key2 # 找出key1、key2集合的并集,该命令可以同时比较更多集合
sunion key1 key2 key3 # 找出key1、key2、key3集合的并集
使用场景:
- 微博,将用户将所有关注的人放在一个set集合中,将他的所有粉丝放在另一个集合中!然后计算每两个用户之间的共同关注、共同爱好、共同粉丝,二度好友(好友推荐)等。
Hash(哈希)
相当于Java中的Map,key-map!其值是一个map集合。
Hash本质和String类型没有太大区别,还是一个简单的key-value
Hash的命令都是以H开头的。
# 设置hash里面字段的值
HSET myhash field1 kuangshen # 设置一个名为myhash的Hash,里面的field1字段的值为kuangshen,如果field1是一个新字段,则返回1(设置多个字段时,返回的是新字段的个数),如果不是,则返回0,设置一个哈希里的一个字段的值,也可以设置一个哈希里的多个字段
HMSET myhash field1 hello field2 world # 设置一个名为myhash的Hash,里面的field1字段的值为hello,field2字段的值为world,如果成功则返回OK,设置一个哈希里多个字段的值
# 获取hash
HGET myhash field1 # 获取myhash的field1的值,获取某个哈希里一个字段的值
HMGET myhash field1 field2 # 同时获取myhash的field1字段和field2字段的值,返回结果是其中的值列表,顺序是按照field1、field2的顺序,获取某个哈希里多个字段的值
HGETALL myhash # 获取名为myhash哈希中的所有键值对,返回结果为键、值相间的列表,即键1、值1、键2、值2...
# 删除字段
HDEL myhash field1 # 删除名为myhash哈希中的field1字段,对应的value值也就删除了,返回值为成功删除的字段个数,也可以支持删除多个字段,如果要删除的字段不存在,则返回0
# 获取hash的大小
HLEN myhash # 获取名为myhash哈希里面的字段的个数
# 判断hash中的字段是否存在
HEXISTS myhash field1 # 判断名为myhash哈希中的field1字段是否存在,如果存在返回1,否则返回0
# 获取hash中所有的字段
HKEYS myhash # 获取名为myhash哈希中所有的字段名
# 获取hash中所有的值
HVALS myhash # 获取名为myhash哈希中所有的值
# 让hash中某个字段自增、自减
HINCRBY myhash field3 1 # 让名为myhash哈希中的field3字段的值自增1,返回值为自增后的值,如果自增的字段不是整型,则报错
HINCRBY myhash field3 2 # 让名为myhash哈希中的field3字段的值自增2,返回值为自增后的值,如果自增的字段不是整型,则报错
HINCRBY myhash field3 -1 # 让名为myhash哈希中的field3字段的值自增-1,相当于自减1
# 如果某个字段不存在,则设置
HSETNX myhash field1 hello # 设置一个名为myhash哈希的field1字段,如果该值不存在,则设置值为hello,否则则不设置,返回值为1表示设置成功,0表示设置失败
使用场景:
- 用于存储经常变动的对象信息,比如用户信息等;
- hash更适合于对象的存储,String更适合于字符串的存储。
Zset(有序集合)
在set的基础上,增加了一个分数字段。set中集合中只有值,zset中集合中每个值都有一个对应的分数,不同的值分数可以相同。
Zset的命令都是以Z开头的。
# 向zset中添加值
ZADD myset 1 one # 向名为myset的有序列表中添加一个one值,其分数为1,该命令的返回值为添加成员的个数,即如果one已经在myset有序列表中存在,这里返回0,否则返回1
ZADD myset 2 two 3 three # 向名为myset的有序队列中添加一个two值,其分数为2,添加一个three值,其分数为3,返回值为添加更新成员的个数,返回值为添加成员的个数,这里如果是首次执行命令,应该返回2,如果two或three已经在myset有序队列中存储,则这里应该返回1或0
# 查看zset中的值(升序或降序)
ZRANGE myset 0 -1 # 查看名为myset有序列表中的所有成员,返回值为按照分数从小到大排好序的列表,如果某几个值的分数是相同的,则这几个值的顺序是乱序的
ZRANGE myset 0 -1 withscores # 查看名为myset有序列表中的所有成员(带分数),返回值为按照分数从小到大排好序的值和其对应分数,如值1,值1,值1对应的分数,值2,值2对应的分数...
ZREVRANGE salary 0 -1 # 查看名为myset有序列表中的所有成员,返回值为按照分数从大到小排好序的列表,如果某几个值的分数是相同的,则这几个值的顺序是乱序的
ZREVRANGE salary 0 -1 withscores # 查看名为myset有序列表中的所有成员(带分数),返回值为按照分数从大到小排好序的值和其对应分数,如值1,值1,值1对应的分数,值2,值2对应的分数...
# 对zset中的值按照指定分数区间排序(升序或降序)
ZRANGEBYSCORE salary -inf +inf # 将salary有序集合中的分数在负无穷到正无穷之间的值从小到大排序进行显示,返回值为排序后的值列表
ZRANGEBYSCORE salary -inf +inf withscores # 将salary有序集合中的分数在负无穷到正无穷之间的值从小到大排序,并带上其分数,返回值为值1,值1对应的分数,值2,值2对应的分数...
ZRANGEBYSCORE salary -inf 2500 withscores # 将salary有序集合中的分数在负无穷到2500之间(闭区间,即<=2500)的值从小到大排序,并带上其分数,返回值为值1,值1对应的分数,值2,值2对应的分数...
ZREVRANGEBYSCORE salary +inf -inf # 将salary有序集合中的分数在正无穷到负无穷之间的值从大到小排序进行显示,返回值为排序后的值列表
ZREVRANGEBYSCORE salary +inf -inf withscores # 将salary有序集合中的分数在正无穷到负无穷之间的值从大到小排序,并带上其分数,返回值为值1,值1对应的分数,值2,值2对应的分数...
ZREVRANGEBYSCORE salary 2500 -inf withscores # 将salary有序集合中的分数在2500到负无穷之间(闭区间,即<=2500)的值从大到小排序,并带上其分数,返回值为值1,值1对应的分数,值2,值2对应的分数...
# 删除一个值
ZREM salary xiaohong # 删除salary有序集合中的xiaohong值,该命令的返回值为删除成功的值的个数
# 获取有序集合中的元素的个数
ZCARD salary # 获取salary有序集合中的值的个数
# 统计指定分数之间的值的个数
ZCOUNT myset 1 3 # 统计myset有序集合中分数在1至3之间(闭区间,即>=1并且<=3)的值的个数
ZCOUNT myset 1 (3 # 统计myset有序集合中分数>=1并且<3的值的个数
ZCOUNT myset (1 (3 # 统计myset有序集合中分数>1并且<3的值的个数
# 计算并集
ZUNIONSTORE out2 2 set1 set2 # 计算set1和set2两个有序集合的并集,并将相同的值的分数相加,然后将并集存储在out2有序集合中
ZUNIONSTORE out2 2 set1 set2 WEIGHTS 2 3 # 计算set1和set2两个有序集合的并集,分数先按照set的顺序乘以权重,然后再相加,比如set1中的值是1:one,2:two,set2中的值是1:one,2:two,3:three,则并集是5:one,9:three,10:two,并集结果放在out2有序集合中
ZUNIONSTORE out3 3 set1 set2 set3 # 计算set1、set2和set3三个有序集合的并集,并将相同的值的分数相加,然后将并集存储在out3有序集合中,所以out3后面的数字,必须和要求并集的有序集合的数量一致,否则语法错误
ZUNIONSTORE out3 3 set1 set2 set3 weights 2 3 4 # 计算set1、set2和set3三个有序集合的并集,分数先按照set的顺序乘以权重,然后再相加,然后将并集存储在out3有序集合中
# 计算交集
ZINTERSTORE in 2 set1 set2 # 计算set1和set2两个有序集合的交集,并将相同的值的分数相加,存储在in有序集合中
ZINTERSTORE in 2 set1 set2 weights 2 3 # 计算set1和set2两个有序集合的交集,并将相同的值的分数先分别乘以权重,然后相加,存储在in有序集合中
ZINTERSTORE in2 3 set1 set2 set3 # 计算set1、set2和set3三个有序集合的交集,并将相同的值的分数相加,存储在in2有序集合中
ZINTERSTORE in2 3 set1 set2 set3 weights 2 3 4 # 计算set1、set2、set3三个有序集合的交集,并将相同的值的分数先分别乘以权重,然后相加,存储在in2有序集合中
使用场景:
- 数据排序(成绩排序、工资排序、排行榜等);
- 带权重的数据排序(重要消息、紧急消息等)。
三种特殊数据类型
geospatial(地理空间)
geospatial底层的实现原理其实就是Zset(有序集合)!
# 地球的两级(南极、北极)是无法添加的
# 有效的经度从-180度到180度,有效的维度从-85.05112878度到05112878度
# 添加地理位置信息到sorted set(有序集合)
GEOADD china:city 116.40 39.90 beijing # 将名称为beijing的坐标添加到以china:city为key的有序集合中,返回值为添加成功的个数,如果beijing的坐标已经被添加了,则返回0,否则返回1
GEOADD china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen # 将名称为chongqing和shenzhen的坐标添加进china:city为key的有序集合中,如果是首次执行,则返回2
# 从某一个key中根据坐标名称获取经纬度
GEOPOS china:city hangzhou # 从key为china:city的集合中获取名为hangzhou的地点的坐标
GEOPOS china:city beijing chongqing # 从key为china:city的集合中获取名为beijing和chongqing的地点的坐标
# 计算两个指定位置之间的直线距离
# 单位:m 表示单位为米、km 表示单位为千米、mi 表示单位为英里、ft 表示单位为英尺
GEODIST china:city beijing shanghai # 返回key为china:city集合中的beijing和shanghai两个地点之间的直线距离,默认单位为米
GEODIST china:city beijing shanghai km # 返回key为china:city集合中的beijing和shanghai两个地点之间的直线距离,指定返回结果的单位为千米
# 以一个坐标为圆心,查询指定半径内所有的地理位置
GEORADIUS china:city 110 30 500 km # 以经度110度,纬度30度为中心,以500千米为半径,查找所有存储在key为china:city的集合中存在的点的名称
GEORADIUS china:city 110 30 500 km withdist # 以经度110度,纬度30度为中心,以500千米为半径,查找所有存储在key为china:city的集合中存在的点的名称,并显示其到(110, 30)点的直线距离
GEORADIUS china:city 110 30 500 km withdist withcoord # 以经度110度,纬度30度为中心,以500千米为半径,查找所有存储在key为china:city的集合中存在的点的名称,并显示其到(110, 30)点的直线距离,和其对应的经度、纬度
GEORADIUS china:city 110 30 500 km count 2 # 以经度110度,纬度30度为中心,以500千米为半径,查找所有存储在key为china:city的集合中存在的点的名称,只显示最近的2个
GEORADIUS china:city 110 30 1000 km desc # 以经度110度,纬度30度为中心,以1000千米为半径,查找所有存储在key为china:city的集合中存在的点的名称,从远到近排序
# 以一个地点为圆心,查询指定半径内所有的地理位置(该命令的参数和GEORADIUS命令相似)
GEORADIUSBYMEMBER china:city beijing 1000 km # 在key为china:city的集合中,以beijing为圆心,查询1000千米以内的点的列表,其返回值中包含beijing本身
# 返回一个或多个位置元素的 Geohash 表示
# Geohash是将二维的经纬度转换成一维的字符串,如果两个地点的哈希值约接近,那么则距离越近
GEOHASH china:city beijing # 返回key为china:city的集合中beijing的哈希值
GEOHASH china:city beijing chongqing # 返回key为china:city的集合中beijing和chongqing的哈希值
我们可以使用Zset命令来操作geospatial!
# 查看GEO所有的地点元素
ZRANGE china:city 0 -1 # 查看key为china:city的有序集合的所有元素
# 移除GEO中的地点元素
ZREM china:city beijing # 移除key为china:city的有序集合中名为beijing的地点信息
使用场景:
- 附近的人;
- 计算两点或两人之间的直线距离等。
Hyperloglog(基数计算)
Hyperloglog是用来做基数统计的算法。
优点:占用的内存是固定的,如果想放2^64(Long类型)不同的元素的基数,只需要12KB的内存。如果要从内存角度比较的话,Hyperloglog是首选!
Hyperloglog有0.81%的错误率,基本可以忽略不计。
# 添加元素
PFADD mykey a b c d e f g h i j # 向mykey集合中添加a b c d e f g h i j元素,如果添加的元素中有原来没有的元素返回1,如果添加的元素在mykey中都 已经有了就返回0
PFADD mykey2 i j z x c v b n m # 向mykey2集合中添加i j z x c v b n m元素
# 统计基数
PFCOUNT mykey # 统计mykey的基数
PFCOUNT mykey2 # 统计mykey2的基数
# 合并集合
PFMERGE mykey3 mykey mykey2 # 将mykey和mykey2集合合并,输出到mykey3集合中,如果mykey3不存在,则创建,如果mykey3已经存在,则是追加
使用场景:
- 统计网页的UV(一个人访问一个网页多次,算一个UV,Unique Visitor)
传统的方式是用set保存用户的id,然后统计set的元素数量作为判断标准,这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了技术,而不是为了保存用户id - 允许容错的其他数量统计类功能
bitmap(位图)
bitmap是按位存储的数据结构,都是通过操作二进制位来进行记录,只有0和1两个状态。
位图不是一种实际的数据类型,而是一组定义在String类型上的面向位操作。因为字符串是二进制安全的blob,它们的最大长度是512 MB,它们适合设置为2^32不同的位。
# 设置某一位的值
SETBIT sign 0 1 # 设置sign元素的第0位的值为1,返回值均为0
SETBIT sign 1 0 # 设置sign元素的第1位的值为0
SETBIT sign 2 0 # 设置sign元素的第2位的值为0
SETBIT sign 3 1 # 设置sign元素的第3位的值为1
SETBIT sign 4 1 # 设置sign元素的第4位的值为1
SETBIT sign 5 0 # 设置sign元素的第5位的值为0
SETBIT sign 6 0 # 设置sign元素的第6位的值为0
# 获取某一位的值
GETBIT sign 3 # 获取sign元素第3位的值
GETBIT sign 6 # 获取sign元素第6位的值
# 统计指定位上值为1的个数
BITCOUNT sign # 统计sign元素所有位上值为1的个数
BITCOUNT sign 0 3 # 统计sign元素第0至3位上被设置为1的bit数,包括第0位,不包括第3位
使用场景:
- 统计疫情感染人数,没有感染的用0表示,感染的用1表示;
- 统计用户信息:是否活跃用户、登录状态、是否打卡等只有两个状态的数据都可以使用bitmap。
事务
Redis事务本质:一组命令的集合(将一组命令按顺序放入队列中,然后一起执行)。一个事务中的所有命令都会被序列化,在事务执行的过程中,会顺序执行!
特性:一次性、顺序性、排他性!执行一系列的命令!
Redis事务没有隔离级别的概念
所有的命令在事务中,并没有直接执行,只有发起执行命令的时候才会真正执行。
Redis的单条命令是保证原子性的,但是Redis事务是不保证原子性的!
Redis事务的三个阶段:
- 开启事务(MULTI)
- 命令入队(前面学过的普通命令)
- 执行事务(EXEC)
正常执行事务的例子:
127.0.0.1:6379> multi # 开启事务
OK
# 命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务
1) OK
2) OK
3) "v2"
4) OK
放弃事务的例子
127.0.0.1:6379> MULTI # 开启事务
OK
# 命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> DISCARD # 放弃事务
OK
127.0.0.1:6379> get k4 # 由于上面放弃了事务,所以事务中的队列都不会被执行,即k4没有被设置,这里就查不到k4了
(nil)
错误的事务——命令错误(类似于Java中的编译型异常),事务中的所有命令都不会被执行
127.0.0.1:6379> MULTI # 开启事务
OK
# 命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> GETSET k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> EXEC #执行事务,报错!所有的命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5 # 因为所有的命令都不会被执行,所以k5没有被设置值
(nil)
错误的事务——语法型错误(类似于Java中的运行时异常),其他命令是可以正常执行的,只有错误命令抛出异常
127.0.0.1:6379> set k1 v1 # 设置k1的值为v1字符串
OK
127.0.0.1:6379> multi # 开启事务
OK
# 命令入队
127.0.0.1:6379(TX)> incr k1 # 将k1的值自增1,但是k1的值默认为字符串
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务
1) (error) ERR value is not an integer or out of range # 虽然第一条命令报错了,但是事务和其他命令依旧执行成功了
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2 # k2被设置成功了
"v2"
127.0.0.1:6379> get k3 # k3被设置成功了
"v3"
127.0.0.1:6379> get k1 # k1还是原来的值,并没有自增
"v1"
监控 Watch
悲观锁:
- 很悲观:认为什么时候都会出问题,无论做什么都会加锁!性能较差!
乐观锁
- 很乐观:认为什么时候都不会出现问题,所以不会上锁!在更新数据的时候去判断一下,在此期间是否有人修改过这个数据!性能较好!
- 获取version
- 更新的时候比较version
事务正常结束:
127.0.0.1:6379> set money 100 # 设置初始有100元钱
OK
127.0.0.1:6379> set out 0 # 设置初始花了0元
OK
127.0.0.1:6379> watch money # 监视money元素
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 20 # money减少20元,即花了20元
QUEUED
127.0.0.1:6379(TX)> INCRBY out 20 # out增加20元
QUEUED
127.0.0.1:6379(TX)> exec # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功!
1) (integer) 80
2) (integer) 20
一旦事务执行成功之后,监控就会自动取消掉!
多线程修改值,使用watch可以当做redis的乐观锁操作
线程1:
127.0.0.1:6379> watch money # 监视money
OK
127.0.0.1:6379> multi # 开启事务
OK
# 命令入队
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> exec #执行事务,执行失败,因为在监视money到执行事务之间,另一个线程(线程2)修改了money的值,导致了当前事务的执行失败
(nil)
线程2:
127.0.0.1:6379> get money # 获取money的值
"80"
127.0.0.1:6379> set money 1000 # 修改money的值为1000
OK
事务执行失败后,还想再次执行的
127.0.0.1:6379> unwatch # 先放弃监视,相当于解锁
OK
127.0.0.1:6379> watch money # 重新开启监视,相当于获取money最新的值
OK
127.0.0.1:6379> multi # 开启事务
OK
# 命令入队
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> incrby out 10
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务
1) (integer) 990
2) (integer) 30
使用场景:
- 电商秒杀
Jedis
我们要使用Java来操作Redis
Jedis是Redis官方推荐的java连接开发工具!使用Java操作Redis的中间件。如果你使用Java操作Redis,那么一定要对Jedis十分的熟悉!
使用步骤:
- 导入对应的依赖;
- 编码测试;
2.1.连接数据库;
2.2.操作命令;
2.3.断开连接。
1、导入对应的依赖
<dependencies>
<!-- 导入jedis的包 -->
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.1.1</version>
</dependency>
<!-- fastjson -->
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
</dependencies>
2、编码测试
public class TestPing {
public static void main(String[] args) {
// 1、new Jedis 对象即可
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
// 验证redis的密码
jedis.auth("123456");
// Jedis 所有的命令就是我们之前学习的所有指令!之前的所有指令在这里就是一个个的方法。
System.out.printf(jedis.ping());
}
}
输出:
常用的API
Redis-Key
public class TestKey {
public static void main(String[] args) {
// 连接redis服务器并输入密码验证
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
jedis.auth("123456");
System.out.println("清空数据:" + jedis.flushDB());
System.out.println("判断某个键是否存在" + jedis.exists("username"));
System.out.println("新增<username, wuhaohua>的键值对:" + jedis.set("username", "wuhaohua"));
System.out.println("新增<password, password>的键值对:" + jedis.set("password", "password"));
System.out.println("系统中所有的键如下:");
Set<String> keys = jedis.keys("*");
System.out.println(keys);
System.out.println("删除键password:" + jedis.del("password"));
System.out.println("判断键password是否存在:" + jedis.exists("password"));
System.out.println("查看键username所存储的值的类型:" + jedis.type("username"));
System.out.println("随机返回key空间的一个:" + jedis.randomKey());
System.out.println("重命名key:" + jedis.rename("username", "name"));
System.out.println("取出改后的name:" + jedis.get("name"));
System.out.println("按索引查询:" + jedis.select(0));
System.out.println("删除当前选择数据库中的所有key:" + jedis.flushDB());
System.out.println("返回当前数据库中key的数目:" + jedis.dbSize());
System.out.println("删除所有数据库中的所有key:" + jedis.flushAll());
}
}
String
public class TestString {
public static void main(String[] args) {
// 连接redis服务器并输入密码验证
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
jedis.auth("123456");
jedis.flushDB();
System.out.println("==========增加数据==========");
System.out.println(jedis.set("key1", "value1"));
System.out.println(jedis.set("key2", "value2"));
System.out.println(jedis.set("key3", "value3"));
System.out.println("删除键key2:" + jedis.del("key2"));
System.out.println("获取键key2:" + jedis.get("key2"));
System.out.println("修改key1:" + jedis.set("key1", "value1Changed"));
System.out.println("获取key1的值:" + jedis.get("key1"));
System.out.println("在key3后面加入值:"+jedis.append("key3", "End"));
System.out.println("key3的值:"+jedis.get("key3"));
System.out.println("增加多个键值对:"+jedis.mset("key01", "value01", "key02", "value02", "key03", "value03"));
System.out.println("获取多个键值对:"+jedis.mget("key01", "key02", "key03"));
System.out.println("获取多个键值对:"+jedis.mget("key01", "key02", "key03", "key04"));
System.out.println("删除多个键值对:"+jedis.del("key01", "key02"));
System.out.println("获取多个键值对:"+jedis.mget("key01", "key02", "key03"));
jedis.flushDB();
System.out.println("==========新增键值对防止覆盖原先值==========");
System.out.println(jedis.setnx("key1", "value1"));
System.out.println(jedis.setnx("key2", "value2"));
System.out.println(jedis.setnx("key2", "value2-new"));
System.out.println(jedis.get("key1"));
System.out.println(jedis.get("key2"));
System.out.println("==========新增键值并设置有效时间==========");
System.out.println(jedis.setex("key3", 2, "value3"));
System.out.println(jedis.get("key3"));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(jedis.get("key3"));
System.out.println("==========获取原值,更新为新值==========");
System.out.println(jedis.getSet("key2", "key2GetSet"));
System.out.println(jedis.get("key2"));
System.out.println("获取key2的值的字符串:"+jedis.getrange("key2", 2, 4));
}
}
List
public class TestList {
public static void main(String[] args) {
// 连接redis服务器并输入密码验证
Jedis jedis = new Jedis("192.168.76.128", 6379);
jedis.auth("123456");
jedis.flushDB();
System.out.println("==========添加一个list==========");
jedis.lpush("collections", "ArrayList", "Vector", "Stack", "WeakHashMap", "LinkedHashMap");
jedis.lpush("collections", "HashSet");
jedis.lpush("collections", "TreeSet");
jedis.lpush("collections", "TreeMap");
// -1代表倒数第一个元素,-2代表倒数第2个元素
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("collections区间0-3的元素:" + jedis.lrange("collections", 0, 3));
System.out.println("===============================");
// 删除列表指定的值,第二个参数为删除的个数(有重复时),后add进去的值先被删除,类似于出栈
System.out.println("删除指定元素个数:" + jedis.lrem("collections", 2, "HashMap"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("删除下标0-3区间之外的元素:" + jedis.ltrim("collections", 0, 3));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("collections列表出栈(左端):" + jedis.lpop("collections"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("collections添加内容,从列表右端,与lpush相对应:" + jedis.rpush("collections", "EnumMap"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("collections列表出栈(右端):" + jedis.rpop("collections"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("修改collections指定下标1的内容:" + jedis.lset("collections", 1, "LinkedArrayList"));
System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
System.out.println("===============================");
System.out.println("collections的长度:" + jedis.llen("collections"));
System.out.println("获取collections下标为2的元素:" + jedis.lindex("collections", 2));
System.out.println("===============================");
jedis.lpush("sortedList", "3", "6", "2", "0", "7", "4");
System.out.println("sortedList排序前:" + jedis.lrange("sortedList", 0, -1));
;
System.out.println(jedis.sort("sortedList"));
;
System.out.println("sortedList排序后" + jedis.lrange("sortedList", 0, -1));
;
}
}
Set
public class TestSet {
public static void main(String[] args) {
// 连接redis服务器并输入密码验证
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
jedis.auth("123456");
jedis.flushDB();
System.out.println("==========向集合中添加元素(不重复)==========");
System.out.println(jedis.sadd("eleSet", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"));
System.out.println(jedis.sadd("eleSet", "e6"));
System.out.println(jedis.sadd("eleSrt", "e6"));
System.out.println("eleSet的所有元素:" + jedis.smembers("eleSet"));
System.out.println("删除一个元素e0:" + jedis.srem("eleSet", "e0"));
System.out.println("eleSet的所有元素:" + jedis.smembers("eleSet"));
System.out.println("删除两个元素e7和e6:" + jedis.srem("eleSet", "e7", "e6"));
System.out.println("eleSet的所有元素:" + jedis.smembers("eleSet"));
System.out.println("随机的移除集合中的一个元素:" + jedis.spop("eleSet"));
System.out.println("随机的移除集合中的一个元素:" + jedis.spop("eleSet"));
System.out.println("eleSet的所有元素:" + jedis.smembers("eleSet"));
System.out.println("eleSet中包含的元素个数:" + jedis.scard("eleSet"));
System.out.println("e3是否在eleSet中:" + jedis.sismember("eleSet", "e3"));
System.out.println("e1是否在eleSet中:" + jedis.sismember("eleSet", "e1"));
System.out.println("e5是否在eleSet中:" + jedis.sismember("eleSet", "e5"));
System.out.println("===============================");
System.out.println(jedis.sadd("eleSet1", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"));
System.out.println(jedis.sadd("eleSet2", "e1", "e2", "e4", "e3", "e0", "e8"));
System.out.println("将eleSet1中删除e1,并存入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e1"));
System.out.println("将eleSet2中删除e2,并存入eleSet3中:" + jedis.smove("eleSet2", "eleSet3", "e2"));
System.out.println("eleSet1中的元素:" + jedis.smembers("eleSet1"));
System.out.println("eleSet3中的元素:" + jedis.smembers("eleSet3"));
System.out.println("==========集合运算==========");
System.out.println("eleSet1中的元素:" + jedis.smembers("eleSet1"));
System.out.println("eleSet2中的元素:" + jedis.smembers("eleSet2"));
System.out.println("eleSet1和eleSet2的交集:" + jedis.sinter("eleSet1", "eleSet2"));
System.out.println("eleSet1和eleSet2的并集:" + jedis.sunion("eleSet1", "eleSet2"));
System.out.println("eleSet1和eleSet2的差集:" + jedis.sdiff("eleSet1", "eleSet2"));// eleSet1中有,eleSet2中没有的元素
// 求交集并将交集保存到eleSet4的集合中
jedis.sinterstore("eleSet4", "eleSet1", "eleSet2");
System.out.println("eleSet4中的元素:" + jedis.smembers("eleSet4"));
}
}
Hash
public class TestHash {
public static void main(String[] args) {
// 连接redis服务器并输入密码验证
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
jedis.auth("123456");
jedis.flushDB();
Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
map.put("key4", "value4");
// 添加名称为hash(key)的hash元素
jedis.hmset("hash", map);
// 向名为hash的hash中添加key为key5,value为value5的元素
jedis.hset("hash", "key5", "value5");
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("散列hash的所有键为:" + jedis.hkeys("hash"));
System.out.println("散列hash的所有值为:" + jedis.hvals("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy("hash", "key6", 6));
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy("hash", "key6", 3));
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("删除一个或者多个键值对:" + jedis.hdel("hash", "key2"));
System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("散列hash中键值对的个数:" + jedis.hlen("hash"));
System.out.println("判断hash中是否存在key2:" + jedis.hexists("hash", "key2"));
System.out.println("判断hash中是否存在key3:" + jedis.hexists("hash", "key3"));
System.out.println("获取hash中的值:" + jedis.hmget("hash", "key3"));
System.out.println("获取hash中的值:" + jedis.hmget("hash", "key3", "key4"));
}
}
所有的API命令,就是我们学习的对应的指令,一个都没有变化!
事务
public class TestTX {
public static void main(String[] args) {
// 连接redis服务器并输入密码验证
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
jedis.auth("123456");
jedis.flushDB();
// 准备数据
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "xiaoming");
String result = jsonObject.toJSONString();
// 开启事务
Transaction multi = jedis.multi();
// jedis.watch(result);
try {
multi.set("user1", result);
multi.set("user2", result);
// 代码抛出异常,事务执行失败
int i = 1 / 0;
// 执行事务
multi.exec();
} catch (Exception ex) {
// 放弃事务
multi.discard();
ex.printStackTrace();
} finally {
// 查询数据
System.out.println("user1的信息:" + jedis.get("user1"));
System.out.println("user2的信息:" + jedis.get("user2"));
// 关闭连接
jedis.close();
}
}
}
SpringBoot整合
SpringBoot操作数据使用的是spring-data jpa jdbc mongodb redis!
SpringData也是和SpringBoot齐名的项目。
说明:SpringBoot 2.x之后,原来使用的jedis被替换成了lettuce。
jedis:底层采用的是直连技术,如果有多个线程操作的话,是不安全的,如果想要避免不安全的问题,就需要使用jedis pool连接池技术!类似于BIO模式(阻塞的)。
lettuce:底层采用的是netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据。类似于NIO模式。
源码分析:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
// 代表默认使用redisTemplate的Bean,我们也可以自己定义一个redisTemplate来替换这个默认的Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的
// 两个泛型都是 Object的类型,我们后面使用需要强制转换,我们期望的其实是<String, Object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
// 由于String是redis中最常使用的类型,所以单独提出来了一个Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
使用步骤:
- 导入依赖
- 配置连接
- 测试!
1、导入依赖
<!-- 操作redis的类库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
2、配置连接
# SpringBoot 所有的配置类,都有一个自动配置类 RedisAutoConfiguration
# 自动配置类都会绑定一个properties配置文件 RedisProperties
# 配置redis
spring.redis.host=xxx.xxx.xxx.xxx
spring.redis.port=6379
spring.redis.password=123456
3、测试!
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redisTemplate
// opsForValue 操作字符串 类似String
// opsForList 操作List 类似List
// opsForSet 操作Set
// opsForHash 操作Hash
// opsForZSet 操作ZSet
// opsForGeo 操作geospatial
// 除了基本的操作,我们常用的方法都可以通过redisTemplate操作,比如实物,和基本的
// 获取redis的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb();
// connection.flushAll();
redisTemplate.opsForValue().set("mykey", "kuangshen");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
阅读RedisTemplate源码:
我们来编写一个自己的RedisTemplate:
package com.kuang.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
// 编写我们自己的RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 我们为了自己开发方便,一般只接使用<String, Objec>泛型
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 配置具体的序列化方式
// Json的序列化
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.setKeySerializer(jackson2JsonRedisSerializer);
// String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
在真实的开发场景中,不会直接使用RedisTemplate类,而是通过封装工具类的方式进行使用。
@Component
public class RedisUtils {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/***
* @param key 键名
* @param time 失效时间(秒)
* @return {@link {@link boolean}} 设置成功返回true,失败返回false
* @Description: 指定缓存失效时间
**/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @return {@link {@link long}} 时间(秒),返回0代表为永久有效
* @Description: 根据key获取过期时间
**/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/***
* @param key 键名
* @return {@link {@link boolean}} 若存在则返回true,若不存在则返回false
* @Description: 判断key是否存在
**/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名,可以传一个值,也可以传多个
* @Description: 删除缓存
**/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
// ==================== String操作 ===============================
/***
* @param key 键名
* @return {@link {@link java.lang.Object}} 值
* @Description: 获取缓存
**/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/***
* @param key 键名
* @param value 值
* @return {@link {@link boolean}} 保存成功返回true,保存失败返回false
* @Description: 设置缓存
**/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param value 值
* @param time 失效时间(秒),若time小于等于0,则不会失效
* @return {@link {@link boolean}} 保存成功返回true,保存失败返回false
* @Description: 设置缓存并设置失效时间
**/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param delta 递增因子(必须大于0)
* @return {@link {@link long}} value自增之后的值
* @throws RuntimeException 自增因子小于等于0时,抛出运行时异常
* @Description: 自增
**/
public long incr(String key, long delta) {
if (delta <= 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/***
* @param key 键名
* @param delta 递减因子(必须大于0)
* @return {@link {@link long}} value自减之后的值
* @throws RuntimeException 自增因子小于等于0时,抛出运行时异常
* @Description: 自减
**/
public long decr(String key, long delta) {
if (delta <= 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().decrement(key, delta);
}
// ==================== Hash操作 ===============================
/***
* @param key 键名
* @return {@link {@link java.util.Map<java.lang.Object,java.lang.Object>}} 对应的多个键值
* @Description: 获取hashKey对应的所有键值
**/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/***
* @param key 键名
* @param map 对应的多个键值
* @return {@link {@link boolean}} 当设置成功时返回true,失败返回false
* @Description: 向key中保存多个键值
**/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param map 对应的多个键值
* @param time 失效时间(秒),必须大于0
* @return {@link {@link boolean}} 当设置成功时返回true,失败返回false
* @Description: 向key中保存多个键值,并设置失效时间
**/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param item 项
* @param value 值
* @return {@link {@link boolean}} 保存成功返回true, 失败返回false
* @Description: 向一张hash表中放入数据,如果不存在则创建
**/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param item 项
* @param value 值
* @param time 失效时间(秒),必须大于0
* @return {@link {@link boolean}}
* @Description: 向一张hash表中放入数据,如果不存在则创建,并设置失效时间
**/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param item 项,可以是一个,也可以是多个
* @Description: 删除hash表中的值
**/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/***
* @param key 键名
* @param item 项
* @return {@link {@link boolean}} 如果有该项的值则返回true,否则返回false
* @Description: 判断hash表中是否有该项的值
**/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/***
* @param key 键名
* @param item 项
* @param by 自增因子(必须大于0)
* @return {@link {@link double}} 自增之后的结果
* @Description: hash递增 如果不存在,就会创建一个
**/
public double hincr(String key, String item, double by) {
if (by <= 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForHash().increment(key, item, by);
}
/***
* @param key 键名
* @param item 项
* @param by 自减因子(必须大于0)
* @return {@link {@link double}} 自减之后的结果
* @Description: hash递增 如果不存在,就会创建一个
**/
public double hdecr(String key, String item, double by) {
if (by <= 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ==================== Set操作 ===============================
/***
* @param key 键名
* @return {@link {@link java.util.Set<java.lang.Object>}} key对应的set的值
* @Description: 根据key获取Set的所有值
**/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
/***
* @param key 键名
* @param value 要查询的值值
* @return {@link {@link boolean}} 若存在则返回true,否则返回false
* @Description: 根据value从Set中查询是否存在
**/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param values 要放入的Set数据,可以是一个,也可以是多个
* @return {@link {@link long}} 本次放入set成功的个数
* @Description: 将数据放入Set缓存中
**/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception ex) {
ex.printStackTrace();
return 0;
}
}
/***
* @param key 键名
* @param time 失效时间(秒),必须大于0
* @param values 要放入的Set数据,可以是一个,也可以是多个
* @return {@link {@link long}} 本次放入set成功的个数
* @Description: 将数据放入Set缓存中,并设置失效时间
**/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception ex) {
ex.printStackTrace();
return 0;
}
}
/***
* @param key 键名
* @return {@link {@link long}} key对应的Set的个数
* @Description: 获取Set缓存的长度
**/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception ex) {
ex.printStackTrace();
return 0;
}
}
/***
* @param key 键名
* @param values 要移除的Set,可以是一个,也可以是多个
* @return {@link {@link long}} 移除的个数
* @Description: 移除值为value的Set
**/
public long setRemove(String key, Object... values) {
try {
return redisTemplate.opsForSet().remove(key, values);
} catch (Exception ex) {
ex.printStackTrace();
return 0;
}
}
// ==================== List操作 ===============================
/***
* @param key 键名
* @param start 开始为止
* @param end 结束为止
* @return {@link {@link java.util.List<java.lang.Object>}} List缓存的内容
* @Description: 获取List缓存的内容
**/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
/***
* @param key 键
* @return {@link {@link long}} 对应List的长度
* @Description: 获取List缓存的长度
**/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception ex) {
ex.printStackTrace();
return 0;
}
}
/***
* @param key 键名
* @param index 索引
* @return {@link {@link java.lang.Object}} 索引对应的值
* @Description: 通过索引获取List中的值
**/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
/***
* @param key 键名
* @param value 需要放入的值
* @return {@link {@link boolean}} 保存成功返回true,失败返回false
* @Description: 将值放入list缓存
**/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param value 需要放入的值
* @param time 失效时间(秒), 必须大于0
* @return {@link {@link boolean}} 保存成功返回true,失败返回false
* @Description: 将值放入list缓存,并设置失效时间
**/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param value 要放入缓存的list
* @return {@link {@link boolean}} 保存成功返回true,失败返回false
* @Description: 将list放入list缓存
**/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/***
* @param key 键名
* @param value 要放入缓存的list
* @param time 失效时间(秒),必须大于0
* @return {@link {@link boolean}} 保存成功返回true,失败返回false
* @Description: 将list放入list缓存,并设置失效时间
**/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键名
* @param index 索引值
* @param value 修改的目标值
* @return {@link Boolean} 修改成功返回true,失败返回false
**/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/**
* 从list中移除count个value值
*
* @param key 键名
* @param count 想要移除的个数
* @param value 要移除的值
* @return {@link Long} 成功移除的个数
**/
public long lRemove(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception ex) {
ex.printStackTrace();
return 0;
}
}
/**
* 移除并获取列表中的(左边)第一个元素
* @param key 键名
* @return {@link Object} 移除的元素
* @date: 2022/2/20
**/
public Object lLeftPop(String key) {
try {
return redisTemplate.opsForList().leftPop(key);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
}
所以所有的redis操作,其实对于java开发人员来说,都十分的简单,更重要的是要理解redis的思想和每一种数据结构的用处和作用场景。
Redis.conf详解
redis配置文件的开头介绍部分:
配置文件中的单位对大小写不敏感。
INCLUDES 包含:
可以使用include命令引入其他配置文件,类似于Spring中的import,JSP中的inclue标签,C语言中的include。
NETWORK 网络配置:
bind 127.0.0.1 -::1 # 绑定的IP,如果注释掉,则是监听所有IP
protected-mode yes # 保护模式是否开启
port 6379 # 服务监听的端口
GENERAL 通用配置:
daemonize yes # 是否以守护进程的方式运行(即后台运行)
pidfile /var/run/redis_6379.pid # 如果已守护进程方式运行,就需要指定一个pid文件
# 日志信息
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 适用于生产环境
# warning (only very important / critical messages are logged)
loglevel notice # 日志级别
logfile "" # 日志文件路径,为空时代表输出到标准输出
databases 16 # 数据库的数量,默认为16个
always-show-logo no # 是否总是显示logo
SNAPSHOTTING 快照配置:
- 在持久化时会用到。在规定的时间内,执行了多少次操作,就会持久化到文件中去,持久化文件有.rdb文件和.aof文件两种。
- redis是内存数据,如果没有持久化,那断电或者关闭后,数据就丢失了。
save 3600 1 # 如果在3600秒(1小时)内有1个key发生了更新,就会持久化
save 300 100 # 如果在300秒(5分钟)内有100个key发生了更新,就会持久化
save 60 10000 # 如果在60秒(1分钟)内有10000个key发生了变化,就会持久化
# 可以根据自己的需求修改这些配置
stop-writes-on-bgsave-error yes # 持久化过程中如果出错了,redis是否还需要继续工作,默认需要继续工作
rdbcompression yes # 是否压缩rdb文件,如果压缩,则需要消耗CPU资源
rdbchecksum yes # 保存rdb文件时,是否进行错误校验
dir ./ # rdb文件生成的目录,默认是配置文件所在的目录
REPLICATION 主从复制的配置:
在后面的Redis主从复制部分详细介绍。
SECURITY 安全配置:
requirepass foobared # 设置登录密码,默认是没有密码的
除了使用配置文件设置密码外,也可以通过命令的方式配置,通过redis-cli客户端连接到redis服务器,然后执行以下命令:
config set requirepass foobared # 设置密码
save # 保存配置命令
config get requirepass # 获取密码
auth foobared # 认证密码,在设置了密码的情况下,连接后需要先认证密码,才能进行操作
CLIENTS 客户端配置:
maxclients 10000 # 设置能连接上redis的最大客户端连接数
MEMORY MANAGEMENT 内存配置:
maxmemory <bytes> # redis服务可以占用的最大内存容量
maxmemory-policy noeviction # 内存达到上限时的处理策略,总共有六种
# 1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
# 2、allkeys-lru : 删除lru算法的key
# 3、volatile-random:随机删除即将过期key
# 4、allkeys-random:随机删除
# 5、volatile-ttl : 删除即将过期的
# 6、noeviction : 永不过期,返回错误
APPEND ONLY MODE AOF模式配置:
appendonly no #是否开启AOF,默认是不开启的,默认是使用rdb方式持久化的,在绝大部分情况下,rdb完全够用
appendfilename "appendonly.aof" # AOF模式生成的持久化的文件的名字
# 持久化方式
# appendfsync always # 每次修改都会同步到aof文件,会消耗性能,所以速度比较慢
appendfsync everysec # 每秒都执行一次同步操作,但可能会丢失1秒内的数据
# appendfsync no # 不执行同步操作,操作系统自己同步数据,速度最快
具体的配置在Redis持久化中详细介绍。
Redis持久化
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会丢失。所以,Redis提供了持久化功能!
RDB(Redis DataBase)
什么是RDB
在主从复制中,rdb文件一般是用于在从机上做备份的。
在指定的时间间隔内,将内存中的数据集快照写入磁盘,也就是Snapshot快照,它恢复时是将快照里文件直接读入内存里。
RDB持久化过程
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化的文件。整个过程中,主进程不进行任何IO操作。这就确保了极高的性能。如果需要大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那么RDB方式要比AOF方式更加高效。
一般情况下,Redis服务端默认使用RDB方式进行持久化。
RDB的相关配置在配置文件的SNAPSHOTTING部分
# The filename where to dump the DB
dbfilename dump.rdb # rdb生成的文件,默认文件名为dump.rdb
dir ./ # rdb文件保存的位置,当前所写的位置为配置文件所在目录
save 60 5 # 如果在60秒(1分钟)内有5个key发生了变化,就会持久化
修改配置文件后,需要重启redis服务。
rdb生成的触发机制:
- 配置文件中save的规则满足的情况下,会自动触发rdb生成;
- 执行flushall命令,也会触发rdb生成;
- 退出redis(即执行shutdown命令),也会触发rdb生成。
备份就会自动生成一个dump.rdb文件。
使用rdb恢复数据:
- 只需要将rdb文件放在配置文件中
dir
配置的目录下,然后启动redis服务,redis在启动时就会自动检查dump.rdb文件,然后恢复其中的数据; - 查看rdb文件保存的位置可以在redis客户端中使用
config get dir
命令查询
[root@localhost redis-6.2.4]# redis-cli
127.0.0.1:6379> config get dir
1) "dir"
2) "/whhdata/server/redis-6.2.4" # 如果在这个目录下存在dump.rdb文件,那么在redis启动时就会自动恢复其中的数据
优点:
- 适合大规模的数据恢复;
- 对数据的完整性要求不高的场景适用。
缺点:
- 需要一定的时间间隔才会进行持久化,如果redis意外宕机了,那最后一次的修改数据可能会丢失;
- fork子进程的时候,会占用一定的内存空间。
AOF(Append Only File)
什么是AOF
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件,但不可以修改文件。Redis启动之初会读该文件重新构建数据,换言之,Redis启动时会根据日志文件的内容将写指令从前到后执行一次,以完成数据的恢复工作。
总结:将我们的所有写命令都记录下来,类似于MySQL的binlog日志,需要恢复的时候,把这个文件全部执行一遍!
AOF相关的配置在配置文件的APPEND ONLY MODE部分
appendonly yes # 是否开启aof持久化机制,默认是no不开启
appendfilename "appendonly.aof" # 生成的aof文件的名字
# 持久化策略
# appendfsync always # 每次数据变化都持久化
appendfsync everysec # 每秒持久化一次
# appendfsync no # 不持久化,依赖操作系统自己同步数据
# 重写规则(AOF默认是对文件进行无限制追加,文件会越来越大)
auto-aof-rewrite-percentage 100 # 超过auto-aof-rewrite-min-size的百分之多少时开始重写
auto-aof-rewrite-min-size 64mb # 如果一个aof文件大于了64M,那就会fork一个新的进程,对aof文件进行重写
修改配置后,重启Redis服务即可生效。
此时,在redis客户端中执行以下命令:
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379> set k4 v4
OK
127.0.0.1:6379> set k5 v5
OK
然后使用cat
或vim
命令查看appendonly.aof文件内容,如下:
*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$2
v1
*3
$3
set
$2
k2
$2
v2
*3
$3
set
$2
k3
$2
v3
*3
$3
set
$2
k4
$2
v4
*3
$3
set
$2
k5
$2
v5
使用aof恢复数据:
1、先将redis服务关闭(在客户端执行shutdown命令),然后删除dump.rdb文件(该文件中也有数据);
2、在
3、除了正常恢复数据外,aof文件还支持被破坏后的修复,但有可能造成数据丢失:
- 将appendonly.aof文件最后一行的前面随便增加一串字符串:
- 此时启动redis服务器,然后使用redis-cli客户端连接服务器就会报错:
- 使用Redis自带的
redis-check-aof
工具修复aof文件:
[root@localhost redis-6.2.4]# redis-check-aof --fix appendonly.aof
0x a4: Expected \r\n, got: 6164
AOF analyzed: size=183, ok_up_to=139, ok_up_to_line=40, diff=44
This will shrink the AOF from 183 bytes, with 44 bytes, to 139 bytes
Continue? [y/N]: y
Successfully truncated AOF
- 使用
vim
命令再次打开appendonly.aof文件,可以看到刚才输入的一串字符串已经没了,但是对应的k5:v5数据的set日志也消失了:
*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$2
v1
*3
$3
set
$2
k2
$2
v2
*3
$3
set
$2
k3
$2
v3
*3
$3
set
$2
k4
$2
v4
- 此时再尝试启动Redis服务,即可正常启动和连接
[root@localhost redis-6.2.4]# redis-server redis.conf
[root@localhost redis-6.2.4]# redis-cli
127.0.0.1:6379> AUTH 123456
OK
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> GET k4
"v4"
127.0.0.1:6379> GET k5
(nil)
优点:
- 每一次修改都同步,文件的完整性会更好;
- 如果配置为每秒同步一次,则只可能丢失一秒内的数据;
- 如果配置为从不同步,则效率最高。
缺点:
- 相对于数据文件来说,aof远大于rdb,修复速度也比rdb慢;
- aof的运行效率也要比rdb慢,所以redis默认的持久化方式是rdb,而不是aof。
扩展
1、如果Redis只拿来做缓存,即只希望数据在服务器运行的时候存在,那也可以不适用任何持久化机制;
2、同时开启RDB和AOF缓存机制时:
- 当Redis启动时,会优先载入AOF文件来恢复数据,因为在通常情况下,AOF文件保存的数据集要比RDB文件保存的数据集更完整;
- RDB的数据并不是实时的,同时使用两种持久化机制重启服务时也只会使用AOF文件恢复数据,但仍不建议单独使用AOF机制,因为RDB更适合用于备份数据库(AOF在不断变化,不方便备份)。
3、性能建议:
- 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就足够了,只需要保留
save 900 1
规则即可; - 如果开启AOF机制,好处是在最恶劣的情况下,只会丢失不超过2秒的数据,启动脚本较简单只需要加载自己的aof文件即可,代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要磁盘许可,应尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设置到5G以上,默认超过原大小100%大小重写,可以改到适当的数值。
- 如果不开启AOF机制,仅靠Master-Slave Replication实现高可用性也是可以的,能够节省一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时挂掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入最新的那个,微博采用的就是这种架构。
Redis发布订阅
Redis发布订阅(publisher/subscriber)是一种消息通信模式:发布者(publisher)发送消息,订阅者(subscriber)接收消息。 如微信、微博的关注系统。
Redis客户端可以订阅任意数量的频道(channel)。
三个角色:消息发布者、频道、消息订阅者。
下图展示频道channel1,以及订阅这个频道的三个客户端——client2、client5和client1之间的关系:
当有新消息通过PUBLISH命令发送给频道channel1时,这个消息就会被发送给订阅它的三个客户端:
Redis发布订阅命令:
这些命令被广泛应用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。
测试命令:
订阅者:
127.0.0.1:6379> SUBSCRIBE kuangshenshuo # 订阅一个频道kuangshenshuo
Reading messages... (press Ctrl-C to quit)
1) "subscribe" # 订阅者
2) "kuangshenshuo" # 订阅的频道名称
3) (integer) 1
# 等待读取推送的信息
1) "message" # 收到一条消息
2) "kuangshenshuo" # 消息来自于哪个频道的名称
3) "hello,kuangshen" # 具体的消息内容
1) "message" # 收到一条消息
2) "kuangshenshuo" # 频道的名称
3) "hello,redis" # 具体的消息内容
发布者:
127.0.0.1:6379> PUBLISH kuangshenshuo "hello,kuangshen" # 发布者发布消息(hello,kuangshen)到频道(kuangshenshuo)
(integer) 1 # 1代表发送成功
127.0.0.1:6379> PUBLISH kuangshenshuo "hello,redis" # 发布者发布消息(hello,redis)到频道(kuangshenshuo)
(integer) 1
原理:
Redis是使用C语言实现的,通过分析Redis源码里的pubsub.c文件,可以了解发布和订阅机制的底层实现,借此加深对Redis的理解。
Redis通过PUBLISH(发布消息)、SUBSCRIBE(根据频道名称订阅频道)和PSUBSCRIBE(订阅一个或多个符合给定模式的频道)等命令实现发布和订阅功能。
通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个的channel,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE命令的关键,就是将客户端添加到给定channel的订阅链表中。
通过PUBLISH命令向订阅者发送消息,redis-server会使用给定的频率作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布之后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景:
1、实时消息系统;
2、实时聊天(频道当做聊天室,将消息回显给所有人即可);
3、订阅、关注系统。
稍微复杂的场景,就会使用消息中间件MQ(Kafka、RabbitMQ等)。
Redis主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower)。数据的复制是单向的,只能从主节点到从节点。Master以写为主,Slave以读为主。
默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括:
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式;
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余;
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时,应用连接主节点,读Redis数据是应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个节点分担读负载,可以大大提供Redis服务器的并发量;
4、高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:
1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
2、从容量上个,单个Redis服务器的容量有限,就算一台Redis服务器的容量为256G,也不能将所有内存都用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。
电商网站上的商品,一般都是一次上传,无数次浏览的,专业说法就是“多读少写”。
对于这种场景,我们可以使用如下的这种架构:
主从复制,读写分离,80%的情况下都是读操作,可以减缓服务器的压力。架构中经常使用。最低配置是一主二从。
手动模式
环境配置
只需要配置从库,不需要配置主库,因为Redis默认自己是主库。
连接Redis客户端之后,可以查看当前库的主从配置信息:
127.0.0.1:6379> info replication # 查看当前库的主从配置信息
# Replication
role:master # 当前库的角色,显示在master
connected_slaves:0 # 当前连接这个库的从机的个数,当前是0
master_failover_state:no-failover
master_replid:f4015fef8c42481e3846dd1d6c4a1d68fcef3ee9
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
通过再复制两个redis.conf
文件,可以同时启动三个Redis服务,从而实现在一台机器上搭建Redis主从复制(一主二从)架构的操作。
[root@localhost redis-6.2.4]# mkdir kconfig # 创建集群配置文件目录
[root@localhost redis-6.2.4]# cp redis.conf kconfig/redis79.conf
[root@localhost redis-6.2.4]# cp redis.conf kconfig/redis80.conf
[root@localhost redis-6.2.4]# cp redis.conf kconfig/redis81.conf
修改配置文件中的内容,使三个Redis服务器能够在不同的端口启动起来,需要修改的配置主要有以下几项:
1、服务监听端口号;
2、开启以守护进程(后台)方式运行;
3、后台进程文件(pid)名称;
4、日志文件(log)名称;
5、rdb持久化(备份)文件名称。
port 6380 # 要保证三台Redis服务器工作在不同的端口上
daemonize yes # 打开以守护进程方(后台)式运行
pidfile /var/run/redis_6380.pid # 修改后台进程文件名称,保证每个服务不重名
logfile "redis6380.log" # 修改日志文件名称,保证每个服务不重名
dbfilename dump6380.rdb # 修改rdb持久化文件名称,保证每个服务不重名
每个服务的以上几项配置都需要进行修改,确保每个服务都能正常启动、运行、关闭。
配置文件修改完成后,启动三个redis服务:
[root@localhost kconfig]# redis-server redis79.conf
[root@localhost kconfig]# redis-server redis80.conf
[root@localhost kconfig]# redis-server redis81.conf
# 查看三个服务是否已经启动
[root@localhost kconfig]# ps -ef | grep redis
root 3053 1 0 15:23 ? 00:00:00 redis-server *:6379
root 3062 1 0 15:24 ? 00:00:00 redis-server *:6380
root 3071 1 0 15:24 ? 00:00:00 redis-server *:6381
root 3094 2336 0 15:25 pts/0 00:00:00 grep --color=auto redis
一主二从
==默认情况下,每个Redis服务都是主节点。==一般情况下,只需要配置从机就可以了。
使用命令配置从机(从机一旦重启,配置就丢失了)
使用redis-cli -p 638x
命令,连接6380和6381两台从机,然后进行配置:
[root@localhost ~]# redis-cli -p 6380 # 连接redis服务
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # 使用SLAVEOF host port命令配置当前服务的主机
OK
127.0.0.1:6380> INFO replication # 查看当前服务的主从配置
# Replication
role:slave # 当前服务的角色:从机
master_host:127.0.0.1 # 当前从机的主机IP
master_port:6379 # 当前从机的主机端口号
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:1af7d504ec7075fe8bb8188c08c25c3ed9482078
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
当80和81两个服务都配置完主机信息后,在主机中查看主从配置信息:
[root@localhost kconfig]# redis-cli -p 6379 # 连接redis服务
127.0.0.1:6379> INFO replication # 查看当前服务的主从配置
# Replication
role:master # 当前服务的角色:从机
connected_slaves:2 # 当前服务的从机个数:2台
slave0:ip=127.0.0.1,port=6380,state=online,offset=336,lag=1 # 第一台从机的信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=336,lag=1 # 第二台从机的信息
master_failover_state:no-failover
master_replid:1af7d504ec7075fe8bb8188c08c25c3ed9482078
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:336
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:336
实际场景下的主从配置,应该在配置文件中配置(现在使用的是命令),否则从机一旦重启,配置就会丢失。
细节:
主机可以写,从机不能写,只能读。主机中所有的信息和数据,都会自动被从机保存。
在主机中写入的数据,可以在从机中查询到:
# 在6379主机中写入数据
127.0.0.1:6379> set k1 v1
OK
# 在6379主机中读取数据
127.0.0.1:6379> get k1
"v1"
# 在6380从机中读取数据
127.0.0.1:6380> get k1
"v1"
# 在6381从机中读取数据
127.0.0.1:6381> get k1
"v1"
如果在从机中写,则会报错:
127.0.0.1:6380> set k2 v2
(error) READONLY You can't write against a read only replica.
在未配置哨兵的情况下,主机如果宕机了,从机不会发现主机宕机,会继续按照从机的工作方式工作,但从机的数据不会丢失。主机重新上线之后,是可以继续和从机同步数据的。
如果是使用命令行(非配置文件)配置的主从,如果从机宕机了,再次启动后,从机会自动变成另一个主机,如果再次通过SLAVEOF
命令配置回从机,则在从机断线这段时间内,主机设置的值,也会立即同步到从机。
主从机之间的复制原理:
Slave启动成功连接到Master后,会发送一个sync命令。Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据的命令,在后台进程执行完毕后,Master将传送整个数据文件到Slave,并完成一次完全同步。
全量复制:而Slave服务在接收到数据库文件后,将其存盘并加载到内存中。
增量复制:Master继续将新的所有收集到的修改命令依次传给Slave,完成同步。
但是只要是重新连接Master,一次完全同步(全量复制)将被自动执行。
使用配置文件配置从机(从机重启时,会根据配置文件重新连接主机)
打开从机的配置文件,然后修改REPLICATION部分的配置即可:
replicaof <masterip> <masterport> # 配置主机的IP地址和端口号
masterauth <master-password> # 如果主机设置了连接密码(requirepass),则在这里配置主机的密码
masteruser <username> # 如果主机设置了用户名(user),则在这里配置主机的用户名
层层链路
与一主二从的结构(两个从节点都连接至主节点)不同,层层链路的结构是,A从节点连接B从节点,B从节点连接主节点:
需要特别注意的是,6380节点在逻辑上既是6379的从节点,同时也是6381的主节点,但是在实际中,6380节点仍然只发挥从节点的作用,不能进行写操作!
谋朝篡位!即使6379又启动了,可以正常服务了,但是由于6380已经成为了主节点,主从结构也不会自动变回原来的样子,如果想要变回原来的样子,需要手动设置。
其配置过程如下:
# 启动三个redis服务
[root@localhost kconfig]# redis-server redis79.conf
[root@localhost kconfig]# redis-server redis80.conf
[root@localhost kconfig]# redis-server redis81.conf
[root@localhost kconfig]# ps -ef | grep redis
root 6890 1 0 21:00 ? 00:00:00 redis-server *:6379
root 6897 1 0 21:00 ? 00:00:00 redis-server *:6380
root 6904 1 0 21:00 ? 00:00:00 redis-server *:6381
root 6909 6800 0 21:00 pts/0 00:00:00 grep --color=auto redis
# 使用redis客户端连接6381服务,然后配置其主机为6380服务
[root@localhost ~]# redis-cli -p 6381
127.0.0.1:6381> SLAVEOF 127.0.0.1 6380 # 配置其主节点为6380服务
OK
127.0.0.1:6381> INFO replication
# Replication
role:slave # 当前服务为从节点
master_host:127.0.0.1 # 主节点IP地址
master_port:6380 # 主节点端口号
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:bcfdba95a84dc7e54e5b71ceee8c6def1d551460
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
# 使用redis客户端连接6380服务,然后配置其主机为6379服务
[root@localhost ~]# redis-cli -p 6380
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # 配置其主节点为6379服务
OK
127.0.0.1:6380> INFO replication
# Replication
role:slave # 当前服务为从节点(虽然当前服务也有从服务,但是当前服务还是从节点,不能进行写操作)
master_host:127.0.0.1 # 主节点IP
master_port:6379 # 主节点端口号
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:252
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=252,lag=1
master_failover_state:no-failover
master_replid:ddc8454cc70fc03633225e266167f787c05a29ad
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:252
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:252
# 使用redis客户端连接6379服务,查看其主从配置信息
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=308,lag=1
master_failover_state:no-failover
master_replid:ddc8454cc70fc03633225e266167f787c05a29ad
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:322
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:322
配置成功后,进行测试:
# 尝试在6380节点进行写操作,系统会报错,所以6380节点不能写
127.0.0.1:6380> set k5 v5
(error) READONLY You can't write against a read only replica.
# 在6379节点进行写操作,然后再6380和6381节点进行数据查询
127.0.0.1:6379> set k6 v6
OK
127.0.0.1:6380> get k6
"v6"
127.0.0.1:6381> get k6
"v6"
如果没有老大了(6379宕机了),在没有哨兵模式的情况下,可以通过
SLAVEOF no one
命令手动修改6380节点为主节点。
具体操作如下:
# 使用SLAVEOF no one命令,修改6380节点的从属关系
127.0.0.1:6380> SLAVEOF no one
OK
127.0.0.1:6380> INFO replication
# Replication
role:master # 修改后,当前节点为主节点
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=920,lag=1
master_failover_state:no-failover
master_replid:cb1285055e0b09ce969738c28316d18631ec52e9
master_replid2:ddc8454cc70fc03633225e266167f787c05a29ad
master_repl_offset:920
second_repl_offset:921
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:920
# 在6380节点进行写操作,然后再6381节点查询数据
127.0.0.1:6380> set k7 v7
OK
127.0.0.1:6381> get k7
"v7"
哨兵模式(自动选举主节点)
概述
主从切换技术的方法是:当主服务器宕机后,需要手动把一台服务器切换为主服务器,这就需要人工干预,费时费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多的时候,我们要优先考虑哨兵模式。Redis从2.8版本开始正式提供了Sentinel(哨兵)架构来解决这个问题。
谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用:
- 通过发送命令,让Redis服务返回监控其运行状态,包括主服务器和从服务器;
- 当哨兵监控到Master宕机,会自动将Slave切换成Master,然后通过发布订阅模式通知其他的从服务,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务宕机,哨兵1先监测到这个情况,系统并不会马上进行failover(故障转移)过程,仅仅是哨兵1主管的认为主服务不可用,这个现象称为主观下线。当后面哨兵也监测到主服务不可用,并且数量达到一定值时,那么哨兵之间会进行一次投票,投票的结果由一个哨兵发起,进行failover(故障转移)操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务实现切换主服务,这个过程称为客观下线。
一主二从单哨兵架构
1、配置哨兵配置文件sentinel.conf:
# sentinel monitor 被监控的名称(自定义的) 监控的redis服务host 监控的redis服务端口 配置多少个哨兵认为主节点失联,则切换主节点
sentinel monitor mymaster 192.168.76.128 6379 1
后面的数字1,代表主机挂了之后,多少个Sentinel认为主节点失联,才会切换主节点,这里必须要配置成小于等于哨兵进程的个数,否则主节点将永远不会被客观下线!!!
2、启动Redis服务:
[root@localhost kconfig]# redis-server redis79.conf
[root@localhost kconfig]# redis-server redis80.conf
[root@localhost kconfig]# redis-server redis81.conf
[root@localhost kconfig]# ps -ef | grep redis
root 8585 1 0 20:39 ? 00:00:00 redis-server *:6379
root 8594 1 0 20:39 ? 00:00:00 redis-server *:6380
root 8603 1 0 20:39 ? 00:00:00 redis-server *:6381
root 8608 8096 0 20:39 pts/0 00:00:00 grep --color=auto redis
3、手动配置Redis主从关系(若主从关系已在配置文件中明确,则忽略此步骤):
登录6380服务,配置其主节点为6379:
[root@localhost kconfig]# redis-cli -p 6380
127.0.0.1:6380> SLAVEOF 192.168.76.128 6379
OK
这里要特别注意,如果需要在其他机器上使用代码的方式访问redis集群的话,这里的IP地址必须写本机的真实IP,而不能是127.0.0.1,
登录6381服务,配置其主节点为6379:
[root@localhost ~]# redis-cli -p 6381
127.0.0.1:6381> SLAVEOF 192.168.76.128 6379
OK
4、启动哨兵进程:
[root@localhost kconfig]# redis-sentinel sentinel.conf
8813:X 28 Feb 2022 20:44:07.447 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
8813:X 28 Feb 2022 20:44:07.447 # Redis version=6.2.4, bits=64, commit=00000000, modified=0, pid=8813, just started
8813:X 28 Feb 2022 20:44:07.447 # Configuration loaded
8813:X 28 Feb 2022 20:44:07.447 * Increased maximum number of open files to 10032 (it was originally set to 1024).
8813:X 28 Feb 2022 20:44:07.447 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.2.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 8813
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
8813:X 28 Feb 2022 20:44:07.449 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
8813:X 28 Feb 2022 20:44:07.451 # Sentinel ID is 444b312ed7090099c49fbdb7934f1855d3c35dda
8813:X 28 Feb 2022 20:44:07.451 # +monitor master mymaster 127.0.0.1 6379 quorum 2
8813:X 28 Feb 2022 20:44:07.452 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
8813:X 28 Feb 2022 20:44:07.455 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
此时,哨兵模式启动成功,从哨兵的日志中也可以看到,其监控了主节点,同时,也发现了主节点下的两个从节点。
测试主节点意外宕机的情况:
1、在主节点写入值,并关闭主节点:
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> SHUTDOWN
2、此时,从节点不会马上升级成为主节点,还需要等待哨兵发现主节点已下线;
127.0.0.1:6380> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379 # 还是原来的主节点
master_link_status:down # 主节点已下线
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:3607
master_link_down_since_seconds:24
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:7d6f61ba0b7c027c839d145d8bd0647989d4edca
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:3607
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:3607
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379 # 还是原来的主节点
master_link_status:down # 主截点已下线
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:9069
master_link_down_since_seconds:173
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:37dc07b4a17ee461d36a56eb0e9f2837386085da
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:9069
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:9069
3、哨兵发现主节点客观下线后,会自动从两个从节点中选举出一个主节点来;
4、此时,分别查看6380和6381服务器的主从服务信息:
# 6380服务
127.0.0.1:6380> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6381 # 其主节点信息已经发生变化
master_link_status:up # 主节点在线
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:44636
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:347b3f04dfcb3cb0dbad83cb7c346087f9ca4dc3
master_replid2:7d6f61ba0b7c027c839d145d8bd0647989d4edca
master_repl_offset:44636
second_repl_offset:3608
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:44636
# 6381服务
127.0.0.1:6381> INFO replication
# Replication
role:master # 6381服务已变成主节点
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=3763,lag=0
master_failover_state:no-failover
master_replid:347b3f04dfcb3cb0dbad83cb7c346087f9ca4dc3
master_replid2:7d6f61ba0b7c027c839d145d8bd0647989d4edca
master_repl_offset:3763
second_repl_offset:3608
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:3763
5、若此时原来的主节点重新上线了,那么也只能归并于新选举出的主节点之下,成为一个从节点。
# 哨兵监听到6379服务再次上线了,并将其主节点设置为6381服务
10335:X 28 Feb 2022 22:51:11.001 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381
10335:X 28 Feb 2022 22:51:20.988 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381
# 使用redis-cli连接6379服务,并查看其主从信息
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6381
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:53339
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:347b3f04dfcb3cb0dbad83cb7c346087f9ca4dc3
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:53339
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:51014
repl_backlog_histlen:2326
优点:
1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有;
2、主从可以切换,故障可以转移,系统的可用性就会更好;
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮;
缺点:
1、Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦;
2、实现哨兵模式的配置其实是非常麻烦的,里面有很多配置项。
哨兵模式的全部配置:
# 哨兵Sentinel实例运行的端口号,默认为26379,如果有哨兵集群,我们还需要配置多个哨兵配置文件,类似于启动Redis集群
port 26379
# 哨兵Sentinel的工作目录,用于存放一些文件
dir /tmp
# 哨兵Sentinel监控的Redis主节点的IP和端口号
# Master-name可以自定义命名主节点名称,只能由大小写字母、数字和".-_"组成
# quorum配置多少个Sentinel哨兵统一认为Master主节点失联,那么这是客观上认为主节点失联,其数量只能小于等于哨兵进程的个数,否则主节点即使宕机也永远不可能客观下线,一般是过半原则(超过一半的哨兵认为主节点下线则客观下线),即(N个哨兵 / 2 + 1)
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass密码配置时,需要进行该配置,以便哨兵连接Redis实例
# 设置哨兵Sentinel连接主从的密码,注意,主从必须设置一样的密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后,主节点没有响应哨兵Sentinel,哨兵则认为主节点主观下线,默认是30000毫秒(即30秒)
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置指定了发生failover主备切换时,最多可以有多少个Slave同时对新的Master进行同步,
# 这个数字越小,完成failover所需要的时间就越长,
# 但是这个数字越大,就意味着,多个Slave因主从切换而不可用。
# 可以通过将这个数值设为 1 来保证每次只有一个Slave处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numreplicas>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间,failover-timeout可以用在以下这些方面
# 1、同一个Sentinel对同一个Master两次failover之间的间隔时间;
# 2、当一个Slave从一个错误的Master那里同步数据开始计算时间,知道Slave被纠正为向正确的Master那里同步数据时;
# 3、当想取消一个正在进行的failover所需要的时间;
# 4、当进行failover时,配置所有Slave指向新的Master所需的最大时间,不过,即使过了这个超时时间,Slave依然会被正确配置为指向Master,但是就不被parallel-sync所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
# 配置当某一事件发生时所需执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发送邮件通知相关人员。
# 对于脚本的运行结果,有以下规则:
# 1、若脚本执行后返回1,那么改脚本稍后会再次被执行,重复次数目前默认为10次;
# 2、若脚本执行后返回2,或者比2大的数,脚本将不会重复执行;
# 3、若脚本在执行过程中由于收到系统的中断信号被终止了,则同返回值为1的行为相同。
# 一个脚本的最大执行时间为60秒,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
# NOTIFICATION SCRIPT通知型脚本
# 当Sentinel有任何警告级别的事件发生时(如Redis实例的主观下线和客观下线等事件),将会调用这个脚本,这是这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在,并且是可执行的,否则Sentinel无法正常启动成功。
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# CLIENTS RECONFIGURATION SCRIPT客户端重新配置主节点参数脚本
# 当一个Master由于failover而发生改变时,这个脚本会被调用,通知相关的客户端关于Master地址已经发生变更的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 参数from-ip、from-port、to-ip、to-port是用来和旧的Master和新的Master(即旧的Slave)通信的,可以用来通知运维人员新旧主节点的IP和端口号。
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
一主二从三哨兵架构
1、修改三个Redis节点的配置文件:
先确保所有的Redis服务已经shutdown。
因为前面启动过Sentinel,所以Sentinel会自动在每个Redis的配置文件最后补充几行配置,为了不影响接下来的部署,所以需要删除掉三个Redis配置文件的最后几行,内容如下:
# redis79.conf文件需要删掉如下内容:
# Generated by CONFIG REWRITE
save 3600 1
save 300 100
save 60 10000
user default on nopass ~* &* +@all
replicaof 127.0.0.1 6381
"redis79.conf" 2059L, 93949C
# redis80.conf文件需要删掉如下内容:
# Generated by CONFIG REWRITE
save 3600 1
save 300 100
save 60 10000
user default on nopass ~* &* +@all
replicaof 127.0.0.1 6381
# redis81.conf文件需要删掉如下内容:
# Generated by CONFIG REWRITE
save 3600 1
save 300 100
save 60 10000
user default on nopass ~* &* +@all
删掉由Sentinel自动补充的配置信息后,需要对三个Redis服务的配置文件作出如下修改:
修改6379节点配置文件:
# 增加密码,增加安全性
requirepass 123456
修改6380节点配置文件:
# 增加密码,与主节点保持相同密码
requirepass 123456
################################# REPLICATION #################################
# 主从复制配置部分
# 配置主节点IP和端口号
replicaof 127.0.0.1 6379
# 配置主节点密码
masterauth 123456
修改6381节点的配置文件:
# 增加密码,与主节点保持一致
requirepass 123456
################################# REPLICATION #################################
# 主从复制配置部分
# 配置主节点IP和端口号
replicaof 127.0.0.1 6379
# 配置主节点密码
masterauth 123456
2、启动三个Redis服务:
[root@localhost kconfig]# redis-server redis79.conf
[root@localhost kconfig]# redis-server redis80.conf
[root@localhost kconfig]# redis-server redis81.conf
[root@localhost kconfig]# ps -ef | grep redis
root 11700 1 0 21:22 ? 00:00:00 redis-server *:6379
root 11707 1 0 21:22 ? 00:00:00 redis-server *:6380
root 11714 1 0 21:22 ? 00:00:00 redis-server *:6381
root 11719 11214 0 21:23 pts/0 00:00:00 grep --color=auto redis
3、使用redis-cli连接三个客户端并进行测试(具体步骤见前文):
4、拷贝三份Sentinel示例配置文件:
[root@localhost kconfig]# cp ../sentinel.conf sentinel79.conf
[root@localhost kconfig]# cp ../sentinel.conf sentinel80.conf
[root@localhost kconfig]# cp ../sentinel.conf sentinel81.conf
5、分别修改三份Sentinel配置文件:
修改sentinel79.conf文件:
[root@localhost kconfig]# vim sentinel79.conf
# 配置文件修改内容
# 哨兵端口号
port 26379
# 以守护进程(后台运行)方式运行,默认是no
daemonize yes
# 修改pidfile路径,避免重名(因为我是在同一台机器上搭建,所以才需要该操作)
pidfile /var/run/redis-sentinel79.pid
# 修改Sentinel日志文件名称
logfile "sentinel79.log"
# 主节点名称、IP、端口号、客观下线票数(一般是过半原则)
sentinel monitor mymaster 127.0.0.1 6379 2
# 主节点密码
sentinel auth-pass mymaster 123456
# 指定多少毫秒之后,主节点没有响应哨兵Sentinel,哨兵则认为主节点主观下线,默认是30000毫秒(即30秒)
sentinel down-after-milliseconds mymaster 30000
# 发生failover主备切换时,最多可以有多少个Slave同时对新的Master进行同步,默认1个
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间,默认三分钟
sentinel failover-timeout mymaster 180000
修改sentinel80.conf文件:
[root@localhost kconfig]# vim sentinel80.conf
# 配置文件修改内容
# 哨兵端口号
port 26380
# 以守护进程(后台运行)方式运行,默认是no
daemonize yes
# 修改pidfile路径,避免重名(因为我是在同一台机器上搭建,所以才需要该操作)
pidfile /var/run/redis-sentinel80.pid
# 修改Sentinel日志文件名称
logfile "sentinel80.log"
# 主节点名称、IP、端口号、客观下线票数(一般是过半原则)
sentinel monitor mymaster 127.0.0.1 6379 2
# 主节点密码
sentinel auth-pass mymaster 123456
# 指定多少毫秒之后,主节点没有响应哨兵Sentinel,哨兵则认为主节点主观下线,默认是30000毫秒(即30秒)
sentinel down-after-milliseconds mymaster 30000
# 发生failover主备切换时,最多可以有多少个Slave同时对新的Master进行同步,默认1个
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间,默认三分钟
sentinel failover-timeout mymaster 180000
修改sentinel81.conf文件:
[root@localhost kconfig]# vim sentinel81.conf
# 配置文件修改内容
# 哨兵端口号
port 26381
# 以守护进程(后台运行)方式运行,默认是no
daemonize yes
# 修改pidfile路径,避免重名(因为我是在同一台机器上搭建,所以才需要该操作)
pidfile /var/run/redis-sentinel81.pid
# 修改Sentinel日志文件名称
logfile "sentinel81.log"
# 主节点名称、IP、端口号、客观下线票数(一般是过半原则)
sentinel monitor mymaster 127.0.0.1 6379 2
# 主节点密码
sentinel auth-pass mymaster 123456
# 指定多少毫秒之后,主节点没有响应哨兵Sentinel,哨兵则认为主节点主观下线,默认是30000毫秒(即30秒)
sentinel down-after-milliseconds mymaster 30000
# 发生failover主备切换时,最多可以有多少个Slave同时对新的Master进行同步,默认1个
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间,默认三分钟
sentinel failover-timeout mymaster 180000
6、分别启动三个哨兵:
[root@localhost kconfig]# redis-sentinel sentinel79.conf
[root@localhost kconfig]# redis-sentinel sentinel80.conf
[root@localhost kconfig]# redis-sentinel sentinel81.conf
[root@localhost kconfig]# ps -ef | grep sentinel
root 12876 1 0 22:30 ? 00:00:00 redis-sentinel *:26379 [sentinel]
root 12897 1 0 22:30 ? 00:00:00 redis-sentinel *:26380 [sentinel]
root 12904 1 0 22:30 ? 00:00:00 redis-sentinel *:26381 [sentinel]
root 12909 12167 0 22:30 pts/0 00:00:00 grep --color=auto sentinel
7、模拟6379(当前的主服务)服务挂掉:
[root@localhost kconfig]# ps -ef | grep redis
root 12350 1 0 21:51 ? 00:00:02 redis-server *:6379
root 12357 1 0 21:51 ? 00:00:02 redis-server *:6380
root 12365 1 0 21:51 ? 00:00:02 redis-server *:6381
root 12876 1 0 22:30 ? 00:00:00 redis-sentinel *:26379 [sentinel]
root 12897 1 0 22:30 ? 00:00:00 redis-sentinel *:26380 [sentinel]
root 12904 1 0 22:30 ? 00:00:00 redis-sentinel *:26381 [sentinel]
root 12921 12167 0 22:31 pts/0 00:00:00 grep --color=auto redis
[root@localhost kconfig]# kill -9 12350
[root@localhost kconfig]# ps -ef | grep redis
root 12357 1 0 21:51 ? 00:00:02 redis-server *:6380
root 12365 1 0 21:51 ? 00:00:02 redis-server *:6381
root 12876 1 0 22:30 ? 00:00:00 redis-sentinel *:26379 [sentinel]
root 12897 1 0 22:30 ? 00:00:00 redis-sentinel *:26380 [sentinel]
root 12904 1 0 22:30 ? 00:00:00 redis-sentinel *:26381 [sentinel]
root 12942 12167 0 22:33 pts/0 00:00:00 grep --color=auto redis
8、使用redis-cli连接6380服务,查看故障转移(主节点切换)的情况:
[root@localhost kconfig]# redis-cli -p 6380
127.0.0.1:6380> AUTH 123456
OK
127.0.0.1:6380> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=42208,lag=1
master_failover_state:no-failover
master_replid:e55f1ce7d3feba71be8b8d46d03f18c252000733
master_replid2:1ad62275c6c978b78501e8aca1f99397648fa62c
master_repl_offset:42474
second_repl_offset:24105
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:42474
可以看到,此时6380节点已经成为了主节点,此时,在6380节点设置一个新的键值,然后再6381节点查询值:
[root@localhost kconfig]# redis-cli -p 6380
127.0.0.1:6380> AUTH 123456
OK
127.0.0.1:6380> SET k2 v2
OK
[root@localhost kconfig]# redis-cli -p 6381
127.0.0.1:6381> AUTH 123456
OK
127.0.0.1:6381> GET k2
"v2"
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:88577
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:e55f1ce7d3feba71be8b8d46d03f18c252000733
master_replid2:1ad62275c6c978b78501e8aca1f99397648fa62c
master_repl_offset:88577
second_repl_offset:24105
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:88577
9、再次将6379服务启动,查看启动后,各redis服务的情况:
[root@localhost kconfig]# redis-cli -p 6379
127.0.0.1:6379> AUTH 123456
OK
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380 # 主节点已经变成了6380节点
master_link_status:down # 但是发现主节点变成了离线状态,经过查询,实际上6380节点是在线的
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1
master_link_down_since_seconds:-1
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:09007a3b577f81c501bdc3cda63840a9a936d7ec
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
查看redis6379.log日志文件(即6379节点的日志文件),使用vtail -n 10 redis6379.log查看日志的最后10行 :
从日志文件中可以看出,6379节点连接6380主节点时,因为密码认证问题导致连接失败了。
查看redis6379.conf配置文件,并切换到配置文件最后一行,添加主节点的密码配置信息:
[root@localhost kconfig]# vim redis79.conf
重新启动6379节点:
# 先使用redis-cli命令连接6379节点,并关闭6379服务
[root@localhost kconfig]# redis-cli -p 6379
127.0.0.1:6379> AUTH 123456
OK
127.0.0.1:6379> SHUTDOWN
not connected> exit
# 再次启动6379节点
[root@localhost kconfig]# redis-server redis79.conf
# 再次使用redis-cli命令连接6379节点,并查看是否连接成功
[root@localhost kconfig]# redis-cli -p 6379
127.0.0.1:6379> AUTH 123456
OK
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up # 可以看到此时主节点在
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:328070
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:e55f1ce7d3feba71be8b8d46d03f18c252000733
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:328070
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:323631
repl_backlog_histlen:4440
10、使用redis-cli连接6380,并在6379测试主从复制状态:
# 连接6380节点
[root@localhost ~]# redis-cli -p 6380
127.0.0.1:6380> AUTH 123456
OK
127.0.0.1:6380> INFO replication
# Replication
role:master
connected_slaves:2 # 可以看到此时有两个从节点
slave0:ip=127.0.0.1,port=6381,state=online,offset=395194,lag=1
slave1:ip=127.0.0.1,port=6379,state=online,offset=395194,lag=1 # 6379节点已经成功连接
master_failover_state:no-failover
master_replid:e55f1ce7d3feba71be8b8d46d03f18c252000733
master_replid2:1ad62275c6c978b78501e8aca1f99397648fa62c
master_repl_offset:395460
second_repl_offset:24105
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:395460
127.0.0.1:6380> SET k3 v3 # 设置键为k3,值为v3的键值对
OK
# 连接6379节点
[root@localhost kconfig]# redis-cli -p 6379
127.0.0.1:6379> AUTH 123456
OK
127.0.0.1:6379> GET k3
"v3"
SpringBoot中整合哨兵模式
参考资料:springBoot 整合 Redis哨兵/读写分离/Lettuce
代码目录结果如下图所示:
1、pom.xml
<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>
2、配置文件:
application.yml
spring:
redis:
password: 123456
timeout: 5000
###################以下为redis哨兵增加的配置###########################
sentinel:
nodes: 192.168.76.128:26379,192.168.76.128:26380,192.168.76.128:26381
master: mymaster
###################以下为lettuce连接池增加的配置###########################
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 1
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 5000
3、RedisTemplate配置:
可以直接复用之前SpringBoot整合单台Redis服务的RedisTemplate配置代码:
@Configuration
public class RedisConfig {
// 编写我们自己的RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 我们为了自己开发方便,一般只接使用<String, Objec>泛型
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 配置具体的序列化方式
// Json的序列化
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.setKeySerializer(jackson2JsonRedisSerializer);
// String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4、Redis哨兵配置:
@Configuration
@ConfigurationProperties(prefix = "spring.redis.sentinel")
public class RedisSentinelConfig {
// 哨兵节点
private Set<String> nodes;
// 主节点名称
private String master;
@Value("${spring.redis.timeout}")
private long timeout;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.lettuce.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.lettuce.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.lettuce.pool.max-wait}")
private long maxWait;
@Value("${spring.redis.lettuce.pool.max-active}")
private int maxActive;
@Bean
public RedisConnectionFactory lettuceConnectionFactory() {
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(master, nodes);
NamedNode master = redisSentinelConfiguration.getMaster();
String name = master.getName();
redisSentinelConfiguration.setPassword(RedisPassword.of(password.toCharArray()));
GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxIdle(maxIdle);
genericObjectPoolConfig.setMinIdle(minIdle);
genericObjectPoolConfig.setMaxTotal(maxActive);
genericObjectPoolConfig.setMaxWaitMillis(maxWait);
//readFrom(ReadFrom.REPLICA) 可设置,设置了就形成读写分离,读会读取从节点,但是因为有复制过程,要能容忍短时间的脏数据,适合对数据要求不太及时的
LettucePoolingClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
.poolConfig(genericObjectPoolConfig).readFrom(ReadFrom.ANY_REPLICA)
.build();
return new LettuceConnectionFactory(redisSentinelConfiguration, lettuceClientConfiguration);
}
public void setNodes(Set<String> nodes) {
this.nodes = nodes;
}
public void setMaster(String master) {
this.master = master;
}
}
5、编写测试代码:
@SpringBootTest
class Redis03SentinelApplicationTests {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
@Test
void contextLoads() throws JsonProcessingException {
// // 获取当前连接的服务器信息
// try {
// RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
// RedisConnection connection = RedisConnectionUtils.getConnection(factory);
// System.out.println(connection.info());
// }catch (Exception ex) {
// ex.printStackTrace();
// }
redisTemplate.opsForValue().set("k1", "v1");
System.out.println(redisTemplate.opsForValue().get("k1"));
User user = new User("张三", 18);
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}
}
代码执行效果:
6、使用命令行查看代码set进去的键值:
[root@localhost kconfig]# redis-cli -p 6381
127.0.0.1:6381> AUTH 123456
OK
127.0.0.1:6381> KEYS *
1) "user"
2) "k1"
3) "mykey"
127.0.0.1:6381> GET user
"\"{\\\"name\\\":\\\"\xe5\xbc\xa0\xe4\xb8\x89\\\",\\\"age\\\":18}\""
需要注意的点:
1、一定要确保Sentinel配置文件中的主节点的IP、端口号、Redis密码要正确;
2、所有Redis服务的密码必须要一样;
3、Redis配置文件和哨兵配置文件中,一定不要使用127.0.0.1这样的IP地址,必须要使用真实的IP,否则可能导致代码报不能连接的错误;
4、配置文件中,不需要明确指定主节点的IP和端口号;
5、如果Redis服务重启了,或者Sentinel服务重启了,一定记得要检查一下对应的配置文件,因为哨兵模式会根据节点和哨兵的变化情况,自动修改对应的配置文件,有的时候可能会导致出现问题;
6、如果redis服务设置了密码,那么即使是默认的主节点的配置文件,也要把masterauth这个配置项配好,以免在主服务意外宕机,然后再重启时,因为没有配置新的主节点的认证密码而导致无法正常连接。
Redis缓存穿透和雪崩
缓存系统的使用场景:前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。
缓存穿透
描述:
用户不断的请求缓存和数据库中都不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。
在流量大时,可能数据库就会挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
1、接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2、布隆过滤器(Bloom Filter),将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力;
3、对不存在的数据也进行缓存,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
但这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿
描述:
缓存击穿是指一个key非常热点,在不停的扛着大并发量,大并发量集中对这一个key进行访问,当这个key失效的瞬间,持续的大并发就会击穿缓存,大量的请求瞬间打在数据库上,导致数据库压力瞬间增大。
例如微博在特定事件时突然宕机。
解决方案:
1、设置热点数据永不过期,如果key不设置过期时间,则不可能出现失效的瞬间,即不可能出现缓存击穿,但是不过期的话,缓存有可能越来越大,当缓存跑满时,redis会根据配置的规则对数据进行清理(如清理掉长时间不被访问的key、随机清理掉一个key等);
2、加互斥锁,使用分布式锁,保证对于每个key同一时间只能有一个线程去查询后端服务,其他线程没有获取分布式锁的权限,因此只需要等待即可,当第一个线程返回后,数据被刷新到了缓存中,后续线程直接从缓存中获取数据即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
雪崩
描述:
在某一个时间段内,缓存中数据大批量集中过期失效,或者缓存宕机,而查询数据量巨大,大量的数据请求直接打在了数据库上,引起数据库压力过大甚至down机。
和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
其实集中过期并不是最致命的,比较致命的是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以抗住压力的。无非就是对数据库产生周期性的压力而已,而缓存服务节点的宕机,对数据库的服务器造成的压力是不可预知的,很有可能瞬间把数据库压垮。
解决方案:
1、Redis高可用,搭建Redis集群,保证即使一台节点挂掉,其他的节点还可以继续工作;
2、限流降级,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如某个key只允许一个线程查询数据库和写缓存,其他线程等待;
3、数据预热,在正式部署之前,先把可能的数据预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中;
4、随机设置缓存数据的过期时间,防止同一时间大量数据过期。