Redis学习笔记(狂神说)

120 阅读45分钟

一、Redis 在 Linux下安装

1、安装gcc编译环境

yum -y install gcc-c++

2、下载安装包至opt目录下

tar -zxvf redis-6.2.10.tar.gz 

3、进入redis-6.2.10目录

4、在redis-6.2.10目录下执行make命令

make && make install

5、查看默认安装目录:usr/local/bin

1678081324292.png

  • redis-benchmarke :性能测试工具,服务启动后运行该命令
  • redis-check-aof:修复有问题的AOF文件
  • redis-check-rdb:修复有问题的RDB文件
  • redis-cli:客户端,操作入口
  • redis-sentinel:redis集群使用
  • redis-server:redis服务端启动命令

6、将默认的redis.conf拷贝到自己定义好的一个路径下,比如/myredis

1678081349550.png

7、修改配置文件,改完配置文件确保生效,记得重启

  • 将默认deamonize no 修改成 deamonize yes 允许后台运行

1678081391076.png

  • 将默认protected-mode yes 修改为protected-mode no 允许其他设备连接,关闭保护模式

1678081420617.png

  • 默认bind 127.0.0.1 修改为本机IP地址或者直接注释掉,否则影响远程IP连接

1678081448933.png

  • 添加redis密码 修改为requirepass你自己的密码

    修改前

1678085601515.png

修改后

1678085671624.png

8、启动redis服务(选择那个配置文件启动)

redis-server redis6.conf #后面的是你所更改的配置文件
  • 使用管道查询redis-server服务是否启动
ps -ef |grep redis| grep -v grep
ps -ef |grep redis

1678081836905.png

9、连接redis服务

redis-cli -p 6379

[有密码 -a password] 1678087854076.png

10、卸载redis

  1. 关闭redis服务
redis-cli shutdown

2.删除/usr/local/bin目录下面与redis相关的文件

1678087854076.png

11、安装过程中出现缺少Python

wget http://cdn.npm.taobao.org/dist/python/3.6.5/Python-3.6.5.tgz #下载安装包
#安装所需依赖
yum install -y zlib*
yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel
#进Python3
cd Python-3.6.5/
#指定安装目录
./configure --prefix=/usr/local/python3 --with-ssl
#编译源文件、正式安装
make && make install
#建立软连接
 ln -s /usr/local/python3/bin/python3 /usr/bin/python3 
 ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3

二、基础知识

1、redis默认有16个数据库(0—15)

1678087921063.png 默认使用的是第0个数据库,可以使用select切换数据库。不同数据库存储不同的值

127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> dbsize #查看数据库大小
(integer) 0
127.0.0.1:6379[3]> 

keys * #查看当前库的所有key的值
flashdb #清除当前数据库
flashall #清除所有数据库

2、Redis是单线程的!

  • Redis是基于内存操作的,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络宽带,既然可以使用单线程来实现,就使用单线程了。
  • Redis是使用C语言写的,官方提供的数据为100000+的QPS1,完成不必同样是使用key-value的Memecache差!

3、Redis为什么那么快?

  • 误区1:高性能的服务器一定是多性能的?
  • 误区2:多线程(CPU上下文会切换)一定比单线程效率高!
  • 核心:redis是将所有数据全部放到内存中的,所以说使用单线程去操作效率就是最高的,多线程(上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案!

三、五大数据类型

1、官方文档

1678089305349.png 翻译

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting), LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

2、Redis-Key

exists key #判断key是存在
move key db #移除key db是代表哪一个库(1-16)
expire key time #设置key的过期时间 time以秒为单位的过期时间
ttl key #查看key还剩余多少过期时间
type key #查看key是什么类型

Redis中文官网 (redis.cn)

Redis命令中心(Redis commands) -- Redis中国用户组(CRUG)

3、String(字符串)

127.0.0.1:6379> set key1 v1 #设置key
OK
127.0.0.1:6379> get key1 #获得值
"v1"
127.0.0.1:6379> exists key1 #判断key是否存在
(integer) 1
127.0.0.1:6379> append key1 "hello" #追加字符串,如果key不存在,就相当于set key
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> strlen key1 #获取key的长度
(integer) 7
127.0.0.1:6379> append key1 ",yang"
(integer) 12
127.0.0.1:6379> get key1
"v1hello,yang"
127.0.0.1:6379> 

127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views #自增1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> decr views #自减1
(integer) 0
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incrby views 10 #指定自增步长
(integer) 10
127.0.0.1:6379> get views
"10"
127.0.0.1:6379> decrby views 5#指定自减步长
(integer) 5
127.0.0.1:6379> get views
"5"

27.0.0.1:6379> get key1
"yanghang"
127.0.0.1:6379> getrange key1 0 3 #截取字符串[0,3]
"yang"
127.0.0.1:6379> getrange key1 0 -1 #获取全部的字符串和get key一样
"yanghang"
127.0.0.1:6379> 

127.0.0.1:6379> set key2 asdffggg
 OK
127.0.0.1:6379> setrange key2 1 11 #替换指定位置开始的字符串 sd->11
(integer) 8
127.0.0.1:6379> get key2
"a11ffggg"

setex(set with expire) #设置过期时间
setnx(set if not exist) #如果这个key不存在就设置
127.0.0.1:6379> setex key3 30 yang
OK
127.0.0.1:6379> ttl key3
(integer) 26
127.0.0.1:6379> get key3
(nil)
127.0.0.1:6379> setnx mykey mogodb
(integer) 1
127.0.0.1:6379> setnx mykey mogodb
(integer) 0
127.0.0.1:6379> 
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #设置多个值
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
127.0.0.1:6379> mget k1 k2 k3 #获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 #原子性操作,要么全部成功,要么全部失败
(integer) 0
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"

存储对象

127.0.0.1:6379> mset user:1:name zhangsan user:1:age 12
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "12"

先get再set

127.0.0.1:6379> getset n1 redis
(nil)
127.0.0.1:6379> getset n1 m1 #如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get n1
"m1"
127.0.0.1:6379> 

4、List

基本的数据类型,列表。在Redis里面,可以将List看成队列,栈,阻塞队列。所有命令都是以l开头。

1678170327108.png

127.0.0.1:6379> lpush list one #将一个值放在链表的头部
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #从左边开始获取具体的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1 #从左边开始获取具体的值[0,1]
1) "three"
2) "two"
127.0.0.1:6379> rpush list right #从右边开始获取具体的值
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> rpop list #从list右边弹出第一个
"right"
127.0.0.1:6379> lpop list #从list左边弹出第一个
"three"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1 #通过下标获得list中的某一个值
"one"
127.0.0.1:6379> llen list #返回列表的长度
(integer) 2
127.0.0.1:6379> lrem list 1 one #移除list集合中指定个数value,精确指定
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "two"
127.0.0.1:6379> rpush mylist hello
(integer) 1
127.0.0.1:6379> rpush mylist hello1 hello2 hello3
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> ltrim mylist 1 2 #通过下标截取指定长度,这个list已经被改变了,截取了只剩下截取的元素
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
##################################################
rpoplpush #移除列表的最后一个元素,将他移动到一个新的列表中
127.0.0.1:6379> rpush mylist hello hello1 hello2
(integer) 3
127.0.0.1:6379> rpoplpush mylist myotherlist
"hello2"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange myotherlist 0 -1
1) "hello2"

##################################
lset #将列表中指定下标的值替换为另外的一个值,更新操作
127.0.0.1:6379> exists list
(integer) 0
127.0.0.1:6379> lset list 0 item #如果不存在列表我们去更新就会报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 0
1) "value1"
127.0.0.1:6379> lset list 0 item #如果存在就会更新当前下标的值
OK
127.0.0.1:6379> lrange list 0 0
1) "item"
127.0.0.1:6379> lset list 1 other #如果不存在就会报错
(error) ERR index out of range
########################################
linsert #将某个具体的value插入到列的某个元素的前面或者后面
127.0.0.1:6379> rpush mylist hell world
(integer) 2
127.0.0.1:6379> linsert mylist before world other
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hell"
2) "other"
3) "world"
127.0.0.1:6379> linsert mylist after world new
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hell"
2) "other"
3) "world"
4) "new"

小结

  • list实际上是一个链表,before Node after,left,right都可以插入值
  • 如果key不存在,创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有的值,空链表,也代表不存在
  • 在两边插入或者改动值,效率最高!中间元素,相对来说效率会底一点
  • 可以使用list做消息队列

5、Set

set中的值是不能存在的

######################################
127.0.0.1:6379> sadd myset hell yang hang #向集合中添加元素
(integer) 3
127.0.0.1:6379> smembers myset #查看所有元素
1) "hang"
2) "yang"
3) "hell"
127.0.0.1:6379> sismember myset yang #判断某个值是否存在于set集合中
######################################
(integer) 1
127.0.0.1:6379> scard myset #查看set集合中有多少个元素
(integer) 3
27.0.0.1:6379> smembers myset
1) "hang"
2) "yang"
3) "hell"
127.0.0.1:6379> srem myset hell
(integer) 1
127.0.0.1:6379> smembers myset
1) "hang"
2) "yang"
######################################
set 是无序不重复集合,抽随机
127.0.0.1:6379> srandmember myset #随机抽取元素
"yang"
127.0.0.1:6379> srandmember myset
"yang"
127.0.0.1:6379> srandmember myset
"yang"
127.0.0.1:6379> srandmember myset
"hang"
127.0.0.1:6379> srandmember myset
"hang"
127.0.0.1:6379> srandmember myset
"yang"
######################################
删除指定的key,随机删除key
127.0.0.1:6379> smembers myset
1) "hang"
2) "yang"
127.0.0.1:6379> spop myset
"yang"
127.0.0.1:6379> smembers myset
1) "hang"
######################################
将一个指定的值,移动到另外的集合中
127.0.0.1:6379> sadd myset hello world yanghang
(integer) 3
127.0.0.1:6379> smembers myset
1) "hello"
2) "world"
3) "yanghang"
127.0.0.1:6379> sadd myset2 other
(integer) 1
127.0.0.1:6379> smove myset myset2 yanghang
(integer) 1
127.0.0.1:6379> smembers myset2
1) "other"
2) "yanghang"
######################################
微博、B站,共同关注(并集)
数据集合类:
 - 差集
 - 交集
 - 并集
 127.0.0.1:6379> sadd key1 a b c d
(integer) 4
127.0.0.1:6379> sadd key2 c d e f
(integer) 4
127.0.0.1:6379> sdiff key1 key2 #差集
1) "a"
2) "b"
127.0.0.1:6379> sinter key1 key2 #交集
1) "d"
2) "c"
127.0.0.1:6379> sunion key1 key2 #并集
1) "c"
2) "f"
3) "d"
4) "a"
5) "b"
6) "e"

小结

微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中,共同关注!六度分割理论!

6、Hash(哈希)

Map集合,key-Map集合!这个时候的值是一个map集合!本质上和String类型没有太大区别

set myhash filed yanghang

127.0.0.1:6379> hset myhash field kuangsheng #set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash field
"kuangsheng"
127.0.0.1:6379> hmset myhash field hello field2 world # set多个key-value
OK
127.0.0.1:6379> hmget myhash field #获取多个字段值
1) "hello"
127.0.0.1:6379> hget myhash field2
"world"
127.0.0.1:6379> hgetall myhash #获取全部的数据
1) "field"
2) "hello"
3) "field2"
4) "world"
127.0.0.1:6379> hdel myhash field #删除hash指定的key字段!对应的value值也就消失了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"
######################################
127.0.0.1:6379> hmset myhash field1 hello field2 world
OK
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> hlen myhash #获取hash的字段数量
(integer) 2
######################################
127.0.0.1:6379> hexists myhash field1 #判断hash指定字段是否存在
(integer) 1
127.0.0.1:6379> hexists myhash field2
(integer) 1
######################################
#只获取field
#只获取value
127.0.0.1:6379> hkeys myhash
1) "field2"
2) "field1"
127.0.0.1:6379> hvals myhash
1) "world"
2) "hello"
(integer) 1
######################################
127.0.0.1:6379> hset myhash field3 5 #指定增量
(integer) 1
127.0.0.1:6379> hincrby myhash field3 2
(integer) 7
127.0.0.1:6379> hincrby myhash field3 -1
(integer) 6
127.0.0.1:6379> hsetnx myhash field4 hello #如果不存在则可以设置
(integer) 1
127.0.0.1:6379> hsetnx myhash field4 world #如果存在则不可以设置
(integer) 0

小结

hash变更的数据 user name age ,尤其是用户信息之类的,经常变动的信息!hash更适合于对象的存储!String更适合字符串存储!

7、Zset(有序集合)

在set的基础上,增加了一个值 set k1 v1 zset k1 score1 v1

127.0.0.1:6379> zadd myset 1 one #添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three #添加多个值
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
####################################################
#排序如何实现
# zrangebyscore key min max
integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 200 xiaoyang
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf #显示全部的用户 从小到大
1) "xiaoyang"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrevrange salary 0 -1 #从大到小排序
1) "zhangsan"
2) "xiaoyang"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #显示全部的用户并且附带成绩
1) "xiaoyang"
2) "200"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores #显示工资小于2500员工的升序排列
1) "xiaoyang"
2) "200"
3) "xiaohong"
4) "2500"
####################################################
#移除(rem)元素
27.0.0.1:6379> zrange salary 0 -1
1) "xiaoyang"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong #移除指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "xiaoyang"
2) "zhangsan"
####################################################
#获取有序集合中的个数
127.0.0.1:6379> zcard salary
(integer) 2
127.0.0.1:6379> zadd myset 1 hello 2 world 3 yang #获取指定区间元素数量
(integer) 3
127.0.0.1:6379> zcount myset 1 3
(integer) 3


四、三种特殊类型数据

1、geospatial 地理位置

朋友的定位,附近的人,打车距离计算

只有六个命令

geoadd 1678606711555.png

# geoadd 添加地理位置
# 规则:两级无法直接添加,一般我们会下载城市数据,直接通过java程序一次性导入!
# 有效的经度从-180到180度
# 有效的纬度从-85.051112878度到85.05112878度
# 当坐标位置超出上述范围时,该命令将返回一个错误
# 127.0.0.1:6379> geoadd chain:city 31.23 121.47 shanghang
# (error) ERR invalid longitude,latitude pair 31.230000,121.470000
# 参数 key 值(纬度 经度 名称)
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijin
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqin 114.05 22.52 shengzheng
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 43.26 xian
(integer) 2

geopos

127.0.0.1:6379> geopos china:city shanghang #获取指定城市的经度和纬度
1) 1) "121.47000163793563843"
   2) "31.22999903975783553"
127.0.0.1:6379> geopos china:city xian hangzhou
1) 1) "108.96000176668167114"
   2) "43.26000111327904563"
2) 1) "120.1600000262260437"
   2) "30.2400003229490224"

geodist

两人之间的距离

单位 m 米 km千米 mi 英里 ft 英尺

127.0.0.1:6379> geodist china:city shanghang xian #上海到西安的直线距离
"1732960.9517"
127.0.0.1:6379> geodist china:city shanghang xian km
"1732.9610"

georadius 以给定的经纬度为中心,找出某一半径的元素

我附近的人!(获取所有附近的人的地址、定位)通过半径来查询

所有数据都需要录入china:city,才会让结果更加精确

127.0.0.1:6379> georadius china:city 110 30 500km #以110、30经纬度为中心,寻找方圆500km的城市
(error) ERR wrong number of arguments for 'georadius' command
127.0.0.1:6379> georadius china:city 110 30 500 km
1) "chongqin"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord #显示他人的定位信息
1) 1) "chongqin"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
127.0.0.1:6379> georadius china:city 100 30 1000 km withdist #显示到中间距离的位置
1) 1) "chongqin"
   2) "629.6756"
127.0.0.1:6379> georadius china:city 100 30 1000000 km withdist withcoord count 2 #筛选出指定的结果
1) 1) "chongqin"
   2) "629.6756"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "shengzheng"
   2) "1627.7179"
   3) 1) "114.04999762773513794"
      2) "22.5200000879503861"
127.0.0.1:6379> georadius china:city 100 30 1000000 km withdist withcoord count 1
1) 1) "chongqin"
   2) "629.6756"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"

georadiusbymember

#找出位于指定元素周围的其他元素
127.0.0.1:6379> georadiusbymember china:city beijin 1000 km 
1) "beijin"
2) "xian"
127.0.0.1:6379> georadiusbymember china:city shanghai 400 km
1) "hangzhou"
2) "shanghai"

geohash 该命令将返回11个字符的Geohash字符串

#将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近!
127.0.0.1:6379> geohash china:city beijin shanghai
1) "wx4fbxxfke0"
2) "wtw3sj5zbj0"

geo 底层的实现原理其实时Zset,我们可以使用Zset来操作

127.0.0.1:6379> zrange china:city 0 -1
1) "chongqin"
2) "shengzheng"
3) "hangzhou"
4) "shanghai"
5) "beijin"
6) "xian"
127.0.0.1:6379> zrem china:city beijin
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqin"
2) "shengzheng"
3) "hangzhou"
4) "shanghai"
5) "xian"

2、Hyperloglog

什么时基数?

A{1,3,4,5,7,7,8} B{1,3,5,7,8}

基数:不重复的元素 = 5 可以接受误差

简介

Redis2.8.9版本就更新了Hyperloglog数据结构

Redis Hyperloglog基数统计的算法

**优点:**占用的内存是固定的 ,2^64不同的元素的技术,只需要12kb内存。从内存角度来说Hyperloglog是首选

网页的UV(一个人访问一个网站多次,但是还是算作一个人

传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为判断!

这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是为了保存用户id;

0.81%错误率!统计UV任务,可以忽略不计

测试使用

127.0.0.1:6379> pfadd mykey q w e r t y u i #创建第一组元素 mykey
(integer) 1
127.0.0.1:6379> pfcount mykey #统计 mykey 元素基数数量
(integer) 8
127.0.0.1:6379> pfadd mykey2 z x v b n m
(integer) 1
127.0.0.1:6379> pfcount mykey2
(integer) 6
127.0.0.1:6379> pfmerge mykey mykey2
OK
127.0.0.1:6379> pfcount mykey
(integer) 14
127.0.0.1:6379> pfcount mykey2
(integer) 6
127.0.0.1:6379> pfmerge mykey3 mykey mykey2 #合并两组 mykey mykey2 => mykey3 并集
OK
127.0.0.1:6379> pfcount mykey3 #看并将的数量
(integer) 14

如果允许容错,那么一定可以使用Hyperloglog!

如果不允许容错,就使用set或者自己的数据类型即可

3、Bitmap

位存储 0 1 0 1 0 1

统计疫情感染人数疫情;统计用户信息、活跃、不活跃!登录、未登录!打卡,365打卡!两个状态都可以使用Bitmap位图,数据结构!都是操作二进制来进行记录,就只有0和1两个状态!

测试

使用Bitmap来记录周一到周日的打卡!

周一:1;周二:0;周三:0;周四:0;周一:1;......

127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 0
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0

查看某一天是否有打卡!

127.0.0.1:6379> getbit sign 3
(integer) 0
127.0.0.1:6379> getbit sign 0
(integer) 1

统计操作,统计打卡的天数

127.0.0.1:6379> bitcount sign #统计这周的打卡记录,就可以看到是否有全勤
(integer) 2

五、事务

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 k2 v2
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#事务队列中的命令都不会被执行
(nil)

编译型异常(代码有问题!命令有错!),事务中所有的命令都不会被执行!

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 k3
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 #所有的命令都不会被执行
(nil)

运行时异常(1/0),如果事务队列中存在语法性错误,那么执行命令的时候,其他命令是可以执行的,错误命令抛出异常!

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr 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
"v2"
127.0.0.1:6379> get k3
"v3"

六、监控 Watch(面试常问)

悲观锁

  • 很悲观,认为什么时候都会出问题,无论做什么都会加锁

乐观锁

  • 很乐观,认为什么时候都不会出问题,所以不会加锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • version
  • 更新的时候比较version

Redis测监视测试

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 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
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20

测试多线程修改值,使用watch可以当作redis的乐观锁操作

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值,这个时候事务就会失败
(nil)

127.0.0.1:6379> unwatch #如果发现事务执行失败,就先解锁
OK
127.0.0.1:6379> watch money #获取最新的值,再次监视,select version
OK
127.0.0.1:6379> clear

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十分的熟悉!

测试

1、导入对应的依赖

<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
 </dependency>

2、编码测试

  • 连接数据库
  • 操作命令
  • 断开连接
  public static void main(String[] args) {
        //1、new Jedis 对象
        Jedis jedis = new Jedis("192.168.176.100",6379);
        //jedis 所有命令就是我们之前学习的命令
        String ping = jedis.ping();
        System.out.println(ping);
    }

输出:

1678778959000.png

八、SpringBoot集成Redis

整合SpringBoot

说明:在SpringBoot2.x之后,原来使用的jedis被替换成了lettcute

jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,采用jedis pool 连接池! 更像BIO模式

lettcute:采用netty,实例可以采用多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式

源码分析

 @Bean
//没有 redisTemplate 这个类就默认生效这个配置类
 @ConditionalOnMissingBean( name = {"redisTemplate"}) //我们可以自己定义一个redisTemplate来替换这个默认的类
 @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
       //默认的RedisTemplate没有过多的设置,对象都是需要序列化!
        //两个泛型都是 Object,Object的类型都需要类型强制转换
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean //由于String 是redis中最常用的一个类型,所以单独的使用这个类
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

1.导入依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>

2.配置连接

spring.redis.host=192.168.176.100
spring.redis.port=6379

3.测试!

测试

package com.yang;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;

@SpringBootTest
class Redis02SpringbootApplicationTests {
    @Resource
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //redisTemplate 操作不同的数据类型
        //opForValue String
        //opForList List
        //除了基本的操作,我们常用的方法都可以通过redisTemplate操作,和事务以及基本的CRUD
        // RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        // connection.flushDb();


        redisTemplate.opsForValue().set("mykey","狂神说");
        System.out.println(redisTemplate.opsForValue().get("mykey"));
    }

}

RedisTemplate 类

序列化配置

@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;

默认的序列化方式是JDK序列化,我们可能会使用Json来序列化

static RedisSerializer<Object> java(@Nullable ClassLoader classLoader) {
		return new JdkSerializationRedisSerializer(classLoader);
	}

对象传输需要序列化

org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.pojo.User] at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:96)

自定义RedisTemplate

package com.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.databind.ser.std.StringSerializer;
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;

import java.net.UnknownHostException;

/**
 * @author hang yang
 * @create 2023-03-22 20:32
 * @description
 */
@Configuration
public class RedisConfig {
   // 固定模板
   //编写我们自己的RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException{
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //json序列化配置
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //json转译
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        template.setKeySerializer(objectJackson2JsonRedisSerializer);

        objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);

       // String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value序列化方式采用jackson
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        //hash的value序列化方式采用jackson
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

所有的redis操作,其实对于java开发人员来说,十分简单,更重要的是去理解redis的思想和每一种数据结构的用处和作用场景!

九、Redis.conf详解

启动的时候,就通过配置文件来启动!

单位

1679628890690.png

1、配置文件nuit单位对大小写不敏感!

包含

1679629065663.png

网络

bind 192.168.176.128  #绑定的ip
protected-mode no #保护模式
port 6379 #端口设置

通用GENERAL

daemonize yes #以守护进程的方式运行,默认是no,我们需要自己开启为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

#日志的文件名
# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile ""


#数据库的数量,默认的是16个
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16



#是否总是显示logo
# By default Redis shows an ASCII art logo only when started to log to the
# standard output and if the standard output is a TTY and syslog logging is
# disabled. Basically this means that normally a logo is displayed only in
# interactive sessions.
#
# However it is possible to force the pre-4.0 behavior and always show a
# ASCII art logo in startup logs by setting the following option to yes.
always-show-logo no

快照

持久化,在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb .aof

redis 是内存数据库,如果没有持久化,那么数据断电即失

# 如果 3600s内,如果至少有1个key进行了修改,我们就进行持久化操作
save 3600 1
# 如果 300s内,如果至少有100个key进行了修改,我们就进行持久化操作
save 300 100
# 如果 60s内,如果至少有10000个key进行了修改,我们就进行持久化操作
save 60 10000
#我们在学习持久化,会自己定义

#持久化出现错误,是否还需要继续工作
stop-writes-on-bgsave-error yes
#是否压缩rdb文件,需要消耗一些cpu资源
rdbcompression yes
#保存rdb文件的时候,进行错误的检查校验
rdbchecksum yes
#rdb文件保存的目录
dir ./

REPLICATION 复制 主从复制

SECURITY 安全

config get requirepass  #获取redis的密码
config set password #设置redis的密码 password 需要带上引号
auth password #redis认证

CLIENTS 限制

maxclients 10000  #设置能连接上redis的最大客户端数量
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" #持久化文件的名字 

# appendfsync always #每次修改都会sync,消耗性能
appendfsync everysec #每秒执行一次sync,可能会丢失这1s的值
# appendfsync no #不执行sync,这个时候操作系统自己同步数据,速度最快

十、Redis持久化

Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能!

在主从复制中,rdb是备用的!

1、RDB(Redis Database)

什么是RDB

1679724726688.png

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshort快照,它恢复时是将快照文件直接读到内存里。

Redis会单独的创建(fork)一个子进程来持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。这个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能会丢失。我们默认的就是RDB,一般情况下不需要修改这个配置

有时候在生产环境中我们会将dump.rdb文件备份

RDB保存的文件是 dump.rdb 都是在我们的配置文件中快照中进行配置的!

1679722434762.png

触发机制

1、save的规则满足的情况下,会自动触发rdb规则

2、执行flushall命令,也会触发我们的rdb规则

3、退出redis,也会产生rdb文件!

备份就自动生成一个dump.rdb

如何恢复rdb文件

1、只需要rdb文件放在我们redis启动目录就可以了,redis启动的时候会检查dump.rdb恢复其中的数据!

2、查看需要存在的位置

27.0.0.1:6379> config get dir
1) "dir"
2) "/opt/redis-7.0.9/myredis" #如果在这个目录下面存在dump.rdb文件,启动就会自动恢复其中的数据

优点

1、适合大规模的数据恢复!dump.rdb

2、如果你对数据的完整性要求不高!

缺点

1、需要一定的时间间隔进程操作!如果redis意外宕机,最后一次修改数据就没有了!

2、fork子进程的时候会占用一定的内存空间!

2、AOF(Append Only File)

将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部再执行一遍!

是什么 1679725473413.png

以日志的形式来记录每一个写的操作,将Redis执行过的所有指令都记录下来(读操作不记录),只许追加但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

AOF保存的是appendonly.aof文件 1679727683122.png

默认是不开启的,我们需要手动进行配置!我们只需要将appendonly改为yes就开启了aof!

1679727610579.png

如果aof文件有错位的,这时候是启动不起来 的,我们需要修复这个aof文件,redis提供了 redis-check-aof --fix 文件名

1679728198273.png

redis-check-aof --fix /opt/redis-7.0.9/myredis/appendonlydir/appendonly.aof.1.incr.aof 将出问题的一部分删除

[root@localhost myredis]# cd /usr/local/bin
[root@localhost bin]# ls
redis-benchmark  redis-check-aof  redis-check-rdb  redis-cli  redis-sentinel  redis-server                
[root@localhost bin]# redis-check-aof --fix /opt/redis-7.0.9/myredis/appendonlydir/appendonly.aof.1.incr.aof 
Start checking Old-Style AOF
0x              a5: Expected \r\n, got: 6461
AOF analyzed: filename=/opt/redis-7.0.9/myredis/appendonlydir/appendonly.aof.1.incr.aof, size=188, ok_up_to=140, ok_up_to_line=40, diff=48
This will shrink the AOF /opt/redis-7.0.9/myredis/appendonlydir/appendonly.aof.1.incr.aof from 188 bytes, with 48 bytes, to 140 bytes
Continue? [y/N]: y
Successfully truncated AOF /opt/redis-7.0.9/myredis/appendonlydir/appendonly.aof.1.incr.aof
[root@localhost bin]# cd /opt/redis-7.0.9/myredis/
[root@localhost myredis]# redis-server redis7.conf 
[root@localhost myredis]# ps -ef|grep redis
root      68905  68641  0 13:53 pts/2    00:00:00 redis-cli -a yanghang -p 6379
root      69969      1  0 15:12 ?        00:00:00 redis-server *:6379
root      69975   9625  0 15:12 pts/1    00:00:00 grep --color=auto redis

重写规则

如果aof文件大于64M,太大了!就会重新fork一个子进程来将我们的aof文件重写!

优点

1、每一次修改都同步,文件的完整性都会更加好!

2、默认的设置是每秒同步一次,可能会丢失一秒的数据!

3、从不同步,效率最高

缺点

1、相对于数据文件来说,aof文件远远大于rdb,修复速度也比rdb慢!

2、aof运行效率要比rdb慢,所以我们的redis默认的是rdb!

十一、发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

Redis客户端可以订阅任意数量的频道。

订阅/发布消息图:

第一个:消息发送者,第二个:频道 第三个:消息订阅者

1679742118048.png

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

img

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

img

1679742404827.png

测试

订阅端:

127.0.0.1:6379> subscribe kuangshengshuo #订阅一个频道kuangshengshuo
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "kuangshengshuo"
3) (integer) 1
#等待读取推送的信息
1) "message" #消息
2) "kuangshengshuo" #那个频道的消息
3) "hello,kuangshen" #消息的具体内容

1) "message"
2) "kuangshengshuo"
3) "hello,redis"

发送端:

127.0.0.1:6379> publish kuangshengshuo "hello,kuangshen" #发布者发布消息到频道
(integer) 1
127.0.0.1:6379> publish kuangshengshuo "hello,redis"
(integer) 1

原理

1679742991295.png

使用场景:

1、实时消息场景!

2、实时聊天(频道当成聊天室,将信息回显给所以人)!

3、订阅、关注系统

稍微复杂场景我们都会使用消息中间件MQ

十二、主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower) ; 数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。默认情况下,每台Redis服务器都是主节点 ;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

img

主从复制的作用 读写分离:主节点写,从节点读,提高服务器的读写负载能力

数据冗余︰主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

故障恢复︰当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 ; 实际上是一种服务的冗余。

负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载 ; 尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

高可用(集群)基石︰除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

环境配置

只配置从库,不用配置主库。

127.0.0.1:6379> info replication #查看当前库的信息
# Replication
role:master #角色
connected_slaves:0 #连接的从机数量
master_failover_state:no-failover 
master_replid:24b0e8a8ed0189f29086fa10ea792ee49495cf41
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

复制三个配置文件,修改对应的信息

1、端口号

2、pid

3、log文件名

4、dump.rdb文件名

修改完成后,启动三个服务,可以通过进程查看

1680330882126.png

一主二从

**默认情况下,每台Redis服务器都是主节点;**我们一般情况下只用配置从机就好了

主机

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2 #从机的配置
slave0:ip=127.0.0.1,port=6380,state=online,offset=98,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=98,lag=1
master_failover_state:no-failover
master_replid:1daa0646071327bf1d9d0315a823a26dbf23d565
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:98
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:98

从机

127.0.0.1:6381> slaveof 127.0.0.1 6379
OK
127.0.0.1:6381> info replication
# Replication
role:slave  #当前角色
master_host:127.0.0.1 #可以看到主机的信息
master_port:6379
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_read_repl_offset:56
slave_repl_offset:56
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:1daa0646071327bf1d9d0315a823a26dbf23d565
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:56
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:57
repl_backlog_histlen:0

真实的主从配置应该在配置文件中配置,这样是永久的。命令的方式配置下一次启动会失效。

# replicaof <masterip> <masterport>

# If the master is password protected (using the "requirepass" configuration
# directive below) it is possible to tell the replica to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the replica request.
#
# masterauth <master-password>

细节

1、主机可以写,从机不能写只能读!主机中的所有信息和数据,都会被从机自动保存

主机写

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 v1
OK

从机读

127.0.0.1:6380> keys *
(empty array)
127.0.0.1:6380> keys *
1) "k1"
127.0.0.1:6380> set k2 v2
(error) READONLY You can't write against a read only replica. #从机不能写

测试

主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候,如果主机回来了,从机依然可以获取主机写的信息.

如果是使用命令行的方式来配置主从,这个时候如果重启了,从机就会变成主机!只要变为原来的从机就会可以获取原来的值。

复制原理

1680332490004.png

层层链路 上一个M链接下一个S

1680334095798.png

这时候可以完成我们的主从复制

如果主机宕机了,能不能自动变成主机呢?

如果主机断开连接了,我们可以手动使用 slaveof on one 让自己变成主机!其他的节点就可以手动连接到最新的这个主节点!如果这个时候原来的主机恢复了,也没有用!

127.0.0.1:6381> slaveof no one #手动变为主机
OK
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:9a31a312396e4b67bcf625244d399fdb394d94fe
master_replid2:30aea419b13b69533e36b3e015048c31682b118a
master_repl_offset:4389
second_repl_offset:4390
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:57
repl_backlog_histlen:4333

哨兵模式

1680335009424.png

1680335299536.png

测试

我们目前的状态是一主二从

1、配置哨兵配置文件 sentinel.conf

#sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1

后面的这个数字1,代表主机挂了,salve投票看让谁来接替成为主机,票数最多的成为主机

2、启动哨兵

[root@localhost myredis]# redis-sentinel sentinel.conf 
3115:X 01 Apr 2023 15:56:15.866 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
3115:X 01 Apr 2023 15:56:15.866 # Redis version=7.0.9, bits=64, commit=00000000, modified=0, pid=3115, just started
3115:X 01 Apr 2023 15:56:15.866 # Configuration loaded
3115:X 01 Apr 2023 15:56:15.867 * Increased maximum number of open files to 10032 (it was originally set to 1024).
3115:X 01 Apr 2023 15:56:15.867 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 7.0.9 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 3115
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

3115:X 01 Apr 2023 15:56:15.868 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
3115:X 01 Apr 2023 15:56:15.870 * Sentinel new configuration saved on disk
3115:X 01 Apr 2023 15:56:15.870 # Sentinel ID is d668cb83f163724817fc63377579187d2e5f0cc3
3115:X 01 Apr 2023 15:56:15.870 # +monitor master myredis 127.0.0.1 6379 quorum 1
3115:X 01 Apr 2023 15:56:15.871 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
3115:X 01 Apr 2023 15:56:15.872 * Sentinel new configuration saved on disk
3115:X 01 Apr 2023 15:56:15.872 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 

如果主节点断开了,就会在从机中投票一个服务器

1680336124618.png

如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式规则!

优点

1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有

2、主从可以切换,故障可以转移,系统的可用性就会更好

3、哨兵模式就是主从复制的升级,手动到自动,更加健壮

缺点

1、Redis不好在线扩容,集群容量一旦达到上线,在线扩容就会十分麻烦

2、实现哨兵模式的配置其实十分麻烦,里面有很多的选择

哨兵模式的全配置

1680336555905.png

1680336628815.png

1680336679677.png

1680336710786.png

十三、Redis缓存穿透和雪崩

1680337272392.png

1680337296264.png

1、缓存雪崩

通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。 1680337446622.png 那么,当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

1680337487652.png

可以看到,发生缓存雪崩有两个原因:

  • 大量数据同时过期;
  • Redis 故障宕机;

不同的诱因,应对的策略也会不同。

大量数据同时过期

针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:

  • 均匀设置过期时间;
  • 互斥锁;
  • 双 key 策略;
  • 后台更新缓存;

1. 均匀设置过期时间

如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。

2. 互斥锁

当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

3. 双 key 策略

我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。

当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。

4. 后台更新缓存

业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新

事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。

解决上面的问题的方式有两种。

第一种方式,后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效,检测到缓存失效了,原因可能是系统紧张而被淘汰的,于是就要马上从数据库读取数据,并更新到缓存。

这种方式的检测时间间隔不能太长,太长也导致用户获取的数据是一个空值而不是真正的数据,所以检测的间隔最好是毫秒级的,但是总归是有个间隔时间,用户体验一般。

第二种方式,在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。

在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。

Redis 故障宕机

针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法有下面这几种:

  • 服务熔断或请求限流机制;
  • 构建 Redis 缓存高可靠集群;

1. 服务熔断或请求限流机制

因为 Redis 故障宕机而导致缓存雪崩问题时,我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。

服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作

为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。

2. 构建 Redis 缓存高可靠集群

服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好通过主从节点的方式构建 Redis 缓存高可靠集群

如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

2、缓存击穿

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题

1680337388573.png

可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。

应对缓存击穿可以采取前面说到两种方案:

  • 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

3、缓存穿透

当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

1680337577526.png

缓存穿透的发生一般有这两种情况:

  • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
  • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;

应对缓存穿透的方案,常见的方案有三种。

  • 第一种方案,非法请求的限制;
  • 第二种方案,缓存空值或者默认值;
  • 第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;

第一种方案,非法请求的限制

当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

第二种方案,缓存空值或者默认值

当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。

我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。

即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

那问题来了,布隆过滤器是如何工作的呢?接下来,我介绍下。

布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。

布隆过滤器会通过 3 个操作完成标记:

  • 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
  • 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
  • 第三步,将每个哈希值在位图数组的对应位置的值设置为 1;

举个例子,假设有一个位图数组长度为 8,哈希函数 3 个的布隆过滤器。

1680337639224.png

在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中

布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。

所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据

总结

缓存异常会面临的三个问题:缓存雪崩、击穿和穿透。

其中,缓存雪崩和缓存击穿主要原因是数据不在缓存中,而导致大量请求访问了数据库,数据库压力骤增,容易引发一系列连锁反应,导致系统奔溃。不过,一旦数据被重新加载回缓存,应用又可以从缓存快速读取数据,不再继续访问数据库,数据库的压力也会瞬间降下来。因此,缓存雪崩和缓存击穿应对的方案比较类似。

而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此,缓存穿透与缓存雪崩、击穿应对的方案不太一样

1680337674566.png

Footnotes

  1. QPS Queries Per Second 是每秒查询率 ,是一台服务器每秒能够响应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准, 即每秒的响应请求数,也即是最大吞吐能力。