Redis
文章目录
- Redis
- 一、NoSQL概述
- 什么是NoSQL
- NoSQL的四大分类
- 二、Redis概述
- 概述
- 启动redis
- 测试性能
- 基础知识
- 三、五大数据类型
- Redis-Key的命令
- String
- List
- Set
- Hash
- Zset(有序集合)
- 四、三种特殊数据类型
- geospatial 地理位置
- Hyperloglogs
- Bitmaps
- 五、事务
- 六、Jedis
- 七、Redis配置文件
- 常用配置:
- Redis的内存淘汰策略
- 八、Redis 持久化
- RDB(Redis DataBase)持久化
- AOF(Append Only File)持久化
- 九、Redis 发布订阅
- 十、Redis 主从复制和哨兵模式
- 主从复制
- 哨兵模式
- 十一、Redis集群
- 十二、缓存穿透、缓存雪崩与缓存击穿
- 缓存穿透
- 缓存雪崩
- 缓存击穿
- 十三、如何保证Redis缓存与数据库的一致性
一、NoSQL概述
什么是NoSQL
NoSQL
NoSQL = Not Only SQL
泛指非关系型数据库
NoSQL 特点
1、方便扩展(数据之间没有关系,很好扩展)
2、大数据量高性能(NoSQL 的缓存记录级,是一种细粒度的缓存,性能会比较高)
3、数据类型是多样型的(不需要事先设计数据库。随取随用,)
4、传统RDBMS 和 NoSQL
代码语言:javascript复制传统的 RDBMS
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row、col
- 数据操作语言、数据的定义语言
- 严格的一致性
- 基础的事务
- ....
代码语言:javascript复制NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储、列存储、文档存储、图形数据库
- 最终一致性
- CAP定理和BASE理论
- 高可用、高性能、高可扩展
- ....
NoSQL的四大分类
KV键值对:
- Redis
- Tair
- memecache
文档型数据库(bson格式 和json一样)
- MongoDB
- MongoDB是一个基于分布式文件存储的数据库,C 编写,主要用来处理大量的文档
- MongoDB是一个介于关系型数据库和非关系型数据库中间的产品。MongoDB是非关系型数据库中功能最丰富、最像关系型数据库的。
- ConthDB
列存储数据库
- HBase
- 分布式文件系统
图关系数据库
- 不是存图形的,放的是关系,比如:朋友圈社交网络,广告推荐。
- Neo4j,InfoGrid、Graph;
二、Redis概述
概述
Redis是什么
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的API。是NoSQL技术之一,也被称为结构化数据库!
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis能干嘛
1、内存存储,持久化,内存中是断电即失,所以说持久化很重要(rdb、aof)
2、效率高,可以用于高速缓存
3、发布订阅系统
4、地图信息分析
5、计时器、计数器(浏览量)
启动redis
测试性能
redis-benchmark
代码语言:javascript复制#测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
基础知识
redis默认有16个数据库
默认使用的是第0个
我们可以使用 select 进行切换数据库
代码语言:javascript复制127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> dbsize #查看db大小
(integer) 0
127.0.0.1:6379[3]>
清除数据库
代码语言:javascript复制127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> keys *
(empty array)
redis是单线程还是多线程,为什么
redis4.0 之前是单线程运行的;redis4.0 后开始支持多线程。
redis4.0 之前使用单线程是的原因:
- 单线程模式方便开发和调试。
- 即使使用单线程模型也能够并发地处理多客户端的请求,主要是因为Redis内部使用了基于epoll的多路复用。
- redis主要的性能瓶颈是内存或网络带宽,而非CPU。
redis在4.0以及之后的版本中引入了惰性删除(也叫异步删除),意思是我们可以使用异步的方式对redis中的数据进行删除。这样处理的好处是不会使redis的主线程卡顿,会把这些操作交给后台线程来执行。
考点(面试题)
1、redis主线程既然是单线程,为什么还这么快?
1、基于内存操作,所有的运算都是内存级别的,所以性能比较高
2、数据结构简单,这些简单的数据结构的查找和操作的时间复杂度都是O(1)
3、多路复用和非阻塞IO,redis使用IO多路复用功能来监听多个socket连接的客户端,这样就可以用一个线程来处理多个情况,从而减少线程切换带来的开销,同时也避免了IO阻塞操作
4、避免上下文切换,省去了线程切换带来的时间和性能上的开销,而且单线程不会导致死锁的发生
2、IO多路复用是什么?
redis使用非阻塞IO,这样读写流程不再是阻塞的,读写方法都是瞬间完成并返回的,也就是它会采用能读多少读多少,能写多少写多少的策略来执行IO操作。这样会存在一个问题就是当我们在执行读操作时有可能只读了一部分数据,当缓冲区满了我们的数据还没写完,那么生效的数据何时写就是一个问题
IO多路复用就是解决上述问题,使用IO多路复用最简单的方法是使用select函数,该函数是操作系统给用户程序的API接口,==用于监控多个文件描述符的可读和可写情况的,这样就可以监控到文件描述符的读写事件了。==当监控到相应的时间之后就可以通知线程处理相应的业务了,这样就保证了redis读写功能的正常执行。
不过现在操作系统基本上不用select函数了,改为调用epoll函数(Linux)了,因为select函数在文件描述符非常多的时候性能非常差。
3、redis6.0 中的多线程?
redis单线程缺点导致redis的QPS很难得到有效提高(虽然已经够快了)。
redis在4.0版本虽然引入了多线程,但该版本的多线程只能用于大数据量的异步删除,对于非删除操作的意义不大
如果我们使用redis多线程就可以分摊redis同步读写IO的压力,以及充分利用多核cpu资源,虽然在redis中使用了IO多路复用,并且是基于非阻塞的IO进行操作的,但是IO操作本身就是阻塞的。
因此在redis6.0 中新增了多线程的功能来提高IO的读写性能,它的主要实现思路是将主线程的IO读写任务拆分给一组独立的线程去执行,这样就可以使用多个socket的读写并行化了,但redis的命令依旧是主线程串行执行的
**注意:**redis6.0 是默认禁用多线程的,但可以通过配置文件redis.conf 中的io-threads-do-reads 等于 true 来开启。除此之外我们还需要设置线程的数量才能正确的开启多线程的功能,修改redis的配置,比如设置 io-threads 4,表示开启4个线程。
设置线程数一定要小于机器的CPU核数
三、五大数据类型
Redis-Key的命令
http://redis.cn/commands.html
String
代码语言:javascript复制127.0.0.1:6379> set key1 v1 #设置值
127.0.0.1:6379> get key1 #获取值
127.0.0.1:6379> keys * #获得所有的值
127.0.0.1:6379> EXISTS key1 #判断某一个key是否存在
127.0.0.1:6379> APPEND key1 hello #追加字符串,如果当前key不存在,就相当于setkey
127.0.0.1:6379> STRLEN key1 #获取字符串的长度
---------------------------------------------------------------------------------
#i
#步长 i =
127.0.0.1:6379> set views 0 #初始化浏览量为0
127.0.0.1:6379> incr views
127.0.0.1:6379> incr views #自增1 浏览量变为1
127.0.0.1:6379> decr views #自减1 浏览量减一
127.0.0.1:6379> INCRBY views 10 #可以设置步长,指定增量!
127.0.0.1:6379> INCRBY views 5
127.0.0.1:6379> DECRBY views 6
(integer) 9
---------------------------------------------------------------------------------
#字符串范围 range
127.0.0.1:6379> set key1 hello,world
OK
127.0.0.1:6379> GETRANGE key1 0 4 #截取字符串[0,3]
"hello"
127.0.0.1:6379> GETRANGE key1 0 -1 #获取全部字符串
"hello,world"
#替换
127.0.0.1:6379> set key1 abcdef
OK
127.0.0.1:6379> SETRANGE key1 2 x #替换指定位置开始的字符串
(integer) 6
127.0.0.1:6379> get key1
"abxdef"
---------------------------------------------------------------------------------
# setex (set with expire) 设置过期时间
# setnx (set if no exist) 不存在再设置 (在分布式锁中会常常使用)
127.0.0.1:6379> setex key1 30 hello #设置key3的值为hello,30秒后过期
OK
127.0.0.1:6379> ttl key1
(integer) 23
127.0.0.1:6379> setnx mykey redis #如果mykey 不存在,创建mykey
(integer) 1
127.0.0.1:6379> keys *
1) "mykey"
127.0.0.1:6379> ttl key1
(integer) -2
127.0.0.1:6379> setnx mykey MongoDB #如果mykey存在,创建失败
(integer) 0
127.0.0.1:6379> get mykey
"redis"
---------------------------------------------------------------------------------
mset
mget
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3 ##同时获取多个值
1) "v1"
2) "v2"
3) "v3"
# msetnx 用于所有给定 key 都不存在时,同时设置一个或多个 key-value 对。
127.0.0.1:6379> msetnx k1 v1 k4 v4 # msetnx 是一个原子性操作,要么一起成功,要么一起失败!
(integer) 0
127.0.0.1:6379> get k4
(nil)
########################
getset #先get然后再set
#如果不存在值,则返回nil
#如果存在值,获取原来的值,并设置新的值
String类似的使用场景:value除了是我们的字符串还可以是数字!
- 计数器
- 统计多单位的数量
- 对象缓存存储
List
基本数据类型,列表
在redis里面,可以把列表当成栈、队列、阻塞队列
所有的 list 命令都是以 l 开头的
代码语言:javascript复制127.0.0.1:6379> LPUSH list one #将一个值或多个值,插入到列表头部(左
127.0.0.1:6379> LRANGE list 0 -1 #获取list中的值
127.0.0.1:6379> LRANGE list 0 1 #通过区间获取具体的值
127.0.0.1:6379> RPUSH list four #将一个值或多个值,插入到列表尾部(右)
---------------------------------------------------------------------------------
127.0.0.1:6379> LPOP list #移出list的第一个元素
127.0.0.1:6379> RPOP list #移出列表的最后一个元素
---------------------------------------------------------------------------------
127.0.0.1:6379> LINDEX list 0 #通过下标获得 list 中的某一个值
---------------------------------------------------------------------------------
Llen
LLEN list #返回列表的长度
---------------------------------------------------------------------------------
移除指定的值!
Lrem list 1 one #移出list 集合中指定个数的value,精确匹配
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "two"
3) "one"
127.0.0.1:6379> lrem list 2 two
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "one"
---------------------------------------------------------------------------------
trim 截断
ltrim list 1 2 #通过下标截取指定的长度,这个list已经被改变了,只剩下截取的值了
---------------------------------------------------------------------------------
rpoplpush #移出列表的最后一个元素,将它移动到新的列表中
---------------------------------------------------------------------------------
lset 将列表中指定下标的值替换未另外一个值,更新操作
注意:修改列表中指定下标的值,要求列表和下标必须存在
---------------------------------------------------------------------------------
linsert #将某个具体的value插入到列表中某个元素的前面或者后面
Set
Set中的值是不能重复的
代码语言:javascript复制---------------------------------------------------------------------------------
sadd #set集合中添加元素
smembers #查看指定set的所有值
sismember #判断某一个元素是否在set集合中
scard #获取set集合中的元素个数
srem #移出set集合中的指定元素
srandmember #随机抽选出一个元素,也可以随机抽选出指定个数的元素(后面带指定个数)
spop #随机删除一些set集合中的元素
smove (set1)(set2)(值) #将一个指定的值,移动到另外一个set中
sdiff #两个set集合的差集
sinter #两个set集合的交集(比如:共同好友就可以这样实现)
sunion #两个set集合的并集
Hash
Map集合,key-map,此时值是一个map集合。本质上和String类型没有太大区别,还是一个简单的 key-value
代码语言:javascript复制127.0.0.1:6379> hset myhash filed1 yang #set一个具体 key-value
127.0.0.1:6379> hget myhash filed1 #获取一个字段值
127.0.0.1:6379> hmset myhash f1 hello f2 world #set多个 key-value
127.0.0.1:6379> hmget myhash f1 f2 #获取多个字段值
127.0.0.1:6379> HGETALL myhash #获取全部的数据
---------------------------------------------------------------------------------
127.0.0.1:6379> hdel myhash f1 #删除hash指定key字段!对应的value值也就消失了
hlen #获取hash表的字段数量
HEXISTS myhash f2 #判断hash中的指定字段是否存在
HKEYS myhash #只获得所有的字段
HVALS myhash #只获得所有的值
HINCRBY #指定增量
hsetnx #如果不存在则可以设置,如果存在则不能设置
应用场景:
- hash存储变更的数据,尤其是用户信息之类的,经常变动的信息!hash更适合于对象的存储,String更加适合字符串存储
Zset(有序集合)
有序集合底层数据结构是跳跃链表
在set的基础上,增加了一个值
代码语言:javascript复制127.0.0.1:6379> zadd myset 1 one #添加一个值
zadd myset 2 two 3 three #添加多个值
ZRANGE myset 0 -1 #获取zset中的所有值
ZRANGEBYSCORE salary -inf inf #显示全部的用户,从小到大
ZRANGEBYSCORE salary -inf inf withscores #显示全部的用户并且附带成绩
ZRANGEBYSCORE salary -inf 3000 #3000一下的全部用户
ZREVRANGEBYSCORE salary inf -inf #显示全部的用户,从大到小
zrem #移除有序集合中的指定元素
zcard #获取有序集合中元素的个数
---------------------------------------------------------------------------------
zcount #获取指定区间的成员数量
应用场景:
- set 排序 存储班级成绩表、工资表排序
- 普通消息置为 1 、重要消息置为 2,带权重进行判断
- 排行榜应用实现,取 Top N 测试
四、三种特殊数据类型
geospatial 地理位置
GEO底层的实现原理其实就是Zset!我们可以使用Zset命令来操作GEO
Hyperloglogs
用于去重计算,可以接受误差
Bitmaps
位运算,可以统计用户活跃数,打卡数之类
五、事务
Redis 事务本质:一组命令的集合。一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行
一次性、顺序性、排他性!执行一序列的命令
Redis 事务没有隔离级别的概念
所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行
Redis 单条命令是保存原子性的,但是事务不保证原子性
redis 的事务
- 开启事务(multi)
- 命令入队(…)
- 执行事务(exec)
代码语言:javascript复制正常执行事务
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
放弃事务
DISCARD 取消事务。事务队列中的命令都不会被执行
代码语言:javascript复制编译型异常(代码有问题!命令有错)事务中所有的命令都不会被执行
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)> exec #执行事务的时候报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k2 #所有的命令都没有执行
(nil)
代码语言:javascript复制运行时异常(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"
监控 Watch
悲观锁:
- 很悲观,认为什么时候都会出问题,无论做什么都会加锁
乐观锁:
- 很乐观,认为什么时候都不会出问题,所以不会上锁。更新数据的时候去判断一下,在此期间是否有人修改过该数据
- 获取 version
- 更新的时候比较 version
Redis 测监视测试
watch相当于内置版本号了,每次修改都会有记录
正常执行成功
代码语言:javascript复制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的乐观锁操作
代码语言:javascript复制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 #执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失败
(nil)
如果修改失败,获取最新的值就好
六、Jedis
要使用 Java 来操作 Redis
什么是Jedis
是 Redis 官方推荐的 Java 连接开发工具,使用 Java 操作 Redis 中间件
代码语言:javascript复制测试
<!--导入jedis的依赖-->
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
</dependencies>
七、Redis配置文件
常用配置:
- daemonize no:redis默认不是以守护进程的方式运作,可以修改为yes启用守护进程 当redis.conf中选项daemonize设置成yes时,代表开启守护进程模式。在该模式下,redis会在后台运行,并将进程pid号写入至redis.conf选项pidfile设置的文件中,此时redis将一直运行,除非手动kill该进程。但当daemonize选项设置成no时,当前界面将进入redis的命令行界面,exit强制退出或者关闭连接工具(putty,xshell等)都会导致redis进程退出。服务端开发的大部分应用都是采用后台运行的模式。
- port 6379:指定Redis的监听端口
- bind 127.0.0.1:redis绑定的主机地址
- database 16:指定数据库的数量
- save<seconds> <change>:指定多长时间内有多少次更新操作,就将数据同步到数据文件
- dbfilename dump.rdb:指定本机数据库文件名,默认值是dump.rdb
- masterauth <master-password>:当master服务设置了密码保护时,salve服务连接master的密码
- requirepass foobared:设置Redis的连接密码,如果配置了连接密码,客户端在连接redis时需要通过**AUTH<password>**命令提供密码,默认是关闭
Redis的内存淘汰策略
1、为数据设置超时时间
- expire key time(以秒为单位)常用方式
- setex(String key,int seconds,String value) 字符串独有方式
除了字符串自己有设置过期时间方法外,其他方法都需要依赖expire方法来设置时间,如果没有设置时间,那缓存就是永不过期。如果设置了时间,之后又不让缓存永不过期,使用persist key
2、采用LRU算法动态将不用的数据删除
- volatile-lru:设定超时时间的数据中,删除最不常用使用的数据
- allkeys-lru:查询所有的key中最不常使用的数据进行删除,这是应用最广泛的策略。
- volatile-random:在已经设定了超时的数据中随机删除
- allkeys-random:查询所有的key,之后随机删除
- volatile-ttl:查询全部设定超时时间的数据,之后排序,将马上要过期的数据进行删除
- noeviction:如果设置为该属性,则不会进行删除操作,在内存溢出时报错返回
- volatile-lfu:所有配置了超时时间的键中删除使用频率最少的键
- allkeys-lfu:从所有键中删除使用频率最少的键
八、Redis 持久化
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了把内存数据持久化到硬盘文件,以及通过备份文件来恢复数据的功能,即Redis持久化机制。
RDB(Redis DataBase)持久化
RDB持久化是把当前Redis中全部数据生成快照保存在硬盘上。RDB持久化可以手动触发,也可以自动触发。
手动触发
save 和 bgsave 命令都可以手动触发RDB持久化
save命令
save 命令会阻塞 Redis服务,直到RDB持久化完成。当Redis服务存储大量数据时,会造成较长时间的阻塞,不建议使用。
bgsave命令
和 save 命令不同的事:Redis 服务一般不会阻塞。Redis进程会执行 fork 操作创建子进程,真正的持久化是在子进程中执行的(调用rdbSave),主进程会继续提供服务。
bgsave命令具体流程:
自动触发
自动触发的 RDB 持久化都是采用 bgsave 的方式,减少 redis 进程的阻塞。自动触达场景:
- 在配置文件中设置了save的相关配置,如 save m,n。它表示在m秒内数据被修改过n次,自动触发 bgsave 操作
- 当从节点做全量复制时,主节点会自动执行 bgsave 操作,并且把生成的 RDB 文件发送给从节点
- 执行 debug reload 命令时,也会自动触发bgsave操作
- 执行 shutdown 命令时,如果没有开启 AOF 持久化也会自动触发 bgsave 操作
RDB的优缺点:
优点:rdb文件是一个紧凑的二进制压缩文件,是redis在某个时间点的全部数据快照,所以使用rdb恢复数据的速度远远比AOF快,适合备份、全量复制、灾难恢复等
缺点:每次进行bgsave 操作都要执行fork操作创建子进程,重量级操作,频繁执行成本过高,所以无法做到实时持久化。(如果服务器意外宕机或者断电,无法做到把全部数据都备份成功)
AOF(Append Only File)持久化
AOF持久化是把每次写命令追加写入日志中,当需要恢复数据时重新执行AOF文件中的命令就可以了。AOF解决了数据持久化的实时性。
AOF持久化默认是关闭的,修改redis.conf配置文件并重启,即可开启AOF持久化功能。
AOF本质是为了持久化,持久化对象是Redis内每一个key的状态,持久化的目的是为了在Reids发生故障重启后能够恢复至重启前或故障前的状态。相比于RDB,AOF采取的策略是按照执行顺序持久化每一条能够引起Redis中对象状态变更的命令,命令是有序的、有选择的。
AOF持久化流程:
- 命令追加(append):所有写命令都会被追加到AOF缓存区(aof_buf)中。
- 文件同步(sync):根据不同策略将AOF缓存区同步到AOF文件中
- 文件重写(rewrite)·:定期对AOF文件进行重写,以达到压缩的目的
- 数据加载(load):当需要恢复数据时,重新执行AOF文件中的命令
文件同步策略
- always:每次写入缓存区都要同步到AOF文件中,硬盘操作比较慢,限制了redis高并发,不建议
- no:每次写入缓存区后不进行同步,同步到AOF文件的操作由操作系统负责。每次同步AOF文件的周期不可控,而且增大了每次同步的硬盘的数据量
- eversec:每次写入缓存区后,由专门的线程每秒钟同步一次,做到了兼顾性能和数据安全。是建议的同步策略,也是默认的策略
文件触发重写操作
手动触发
直接调用bgrewriteaof命令,如果当时无子进程执行会立刻执行,否则安排在子进程结束后执行。
自动触发
自动触发由Redis的周期性方法 serverCron 检查在满足一定条件时触发
两个参数:
- auto-aof-rewrite-percentage:代表当前AOF文件大小(aof_current_size)和上一次重写后AOF文件大小(aof_base_size)相比,增长的比例。
- auto-aof-rewrite-min-size:表示运行BGREWRITEAOF时AOF文件占用空间最小值,默认为64MB;
当同时满足这两个条件时,AOF文件重写就会触发。
九、Redis 发布订阅
Redis 发布订阅是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道
图示:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
Redis 发布订阅命令
这些命令被广泛用于构件即时通信应用,比如网络聊天室、实时推送等
测试:
订阅端:
代码语言:javascript复制127.0.0.1:6379> SUBSCRIBE qq #订阅一个频道 qq
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "qq"
3) (integer) 1
#等待推送的消息
1) "message" #消息
2) "qq" #哪个频道的消息
3) "hello,qq" #消息的内容
1) "message"
2) "qq"
3) "welcome to qq"
发送端:
代码语言:javascript复制127.0.0.1:6379> PUBLISH qq "hello,qq" #发送者发布消息到频道
(integer) 1
127.0.0.1:6379> PUBLISH qq "welcome to qq" #发送者发布消息到频道
(integer) 1
127.0.0.1:6379>
十、Redis 主从复制和哨兵模式
主从复制
主从复制:把一台Redis服务器的数据复制到其他Redis服务器上,前者称为主节点Master,后者称为从节点Slave,数据的复制是单向的,只能从Master单向复制到Slave,一般Master以写操作为主,Slave以读操作为主,实现读写分离。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点,但一个从节点只能有一个主节点。
主从复制的作用:
- **数据冗余:**主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
- **故障恢复:**当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复
- **负载均衡:**在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- **高可用基石:**主从复制还是哨兵和集群能够实施的基础,故说主从复制是Redis高可用的基础。
**全量复制:**从机每次连接主机时会全量复制,把主机的全部数据复制到主机
**增量复制:**从机连上主机后,对于主机后面更新的数据,会只针对这部分数据同步更新给主机。
哨兵模式
**核心功能:**在主从复制的基础上,哨兵引入了主节点的自动故障转移。
哨兵Sentinel会作为一个独立的进程独立运行,通过发送命令,等到Redis服务器响应,从而监控运行的多个Redis服务器。
原理:哨兵是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的Master并将所有Slave连接到新的Master。所以整个运行哨兵的集群的数量不得少于3个节点
哨兵模式的作用:
- 监控:哨兵会不断地检查主节点和从节点是否运作正常
- 自动故障转移:当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
- 通知提醒:哨兵可以将故障转移的结果发送给客户端
故障转移机制:
- 由哨兵节点定期监控发现主节点是否出现故障,每个哨兵进程会使用 ping 命令检测它自己和主、从库以及其他哨兵节点的网络情况。如果主节点在一定范围内不回复或者是回复一个错误消息,那么该哨兵就会认为这个主节点主管下线了(单方面的)。当超过半数哨兵节点认为该主节点主管下线了,这样就客观下线了。
- 当主节点出现故障,此时哨兵节点会通过Raft算法(投票算法)实现选举机制共同选举出一个哨兵节点为 leader,来负责处理主节点的故障转移和通知。
- 由leader哨兵节点执行故障转移
- 将某一个从节点升级为新的主节点,让其他从节点指向新的主节点
- 若原主节点恢复,也变为从节点,让其他从节点指向新的主节点
- 通知客户端主节点已经更换
注意:客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观操作和故障转移操作。
主节点的选举:
- 过滤掉已下线、没有回复哨兵ping响应的从节点
- 选择配置文件中从节点优先级配置最高的(replica-priority,默认值为100)
- 选择复制进度最快,也就是复制最完整的从节点
十一、Redis集群
什么是Redis集群
Redis3.0加入了Redis集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的master节点上面,从而解决海量数据的存储问题。对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一Redis实例一样。
Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点
为什么需要Redis集群
单实例的Redis缓存足以应对大多数的使用场景,也能实现主从故障迁移。但是在某些场景下,单实例Redis缓存会存在的几个问题:
1、写并发
Redis单实例读写分离可以解决读操作的负载均衡,但对于写操作,仍然是全部落在了master节点上面,在海量数据高并发场景,一个节点写数据容易出现瓶颈,造成master节点的压力上升。
2、海量数据的存储压力
单实例Redis本质上只有一台Master作为存储,如果面对海量数据的存储,一台Redis的服务器就应付不过来了,而且数据量太大意味着持久化成本高,严重时可能会阻塞服务器,造成服务请求成功率下降,降低服务的稳定性。
Redis集群解决了存储能力受到单机限制,写操作无法负载均衡的问题。
十二、缓存穿透、缓存雪崩与缓存击穿
缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求到要到数据库去查询,造成缓存穿透。
解决方案:
1、缓存空对象:如果一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但他的过期时间会很短,一般不超过5分钟
缓存空对象带来的问题:
- 空值做了缓存,意味着缓存中存了更多的键,需要更多的内存空间,较有效的方法是针对这类数据设置一个较短的过期时间,让其自动删除
- 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象
2、布隆过滤器拦截:在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
缓存雪崩
由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一段时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩
解决方案:
1、加锁排队:在缓存失效后,通过加锁或者队列来控制数据库写缓存的线程数量。⽐如对某个 key 只允许⼀个线程查询数据和写缓存,其他线程等待;
2、数据预热:可以通过缓存reload机制,预先去更新缓存,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存实现的时间点尽量均匀;
3、可以把缓存层设计成高可用的,即使个别节点。个别机器宕机,依然可以提供服务。
4、采用多级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其它级别缓存兜底、
缓存击穿
缓存击穿是指当前key是一个热点key(例如一个秒杀活动),并发量非常大,大并发集中对这一个点进行访问,当这个点(缓存)失效的瞬间,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。
解决方案:
1、设置热点数据永不过期
从缓存层面看,没有设置过期时间,所以不会出现热点key过期后产生的问题
2、加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁。
十三、如何保证Redis缓存与数据库的一致性
四种同步策略
1、先更新缓存,再更新数据库
2、先更新数据库,再更新缓存
3、先删除缓存,再更新数据库
4、先更新数据库,再删除缓存
更新缓存:
优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况
缺点:更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取给数据
删除缓存:
优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除
缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。(一般情况下,删除缓存是更优的方案)
先删除缓存再更新数据库:
先更新数据库再删除缓存
第一种和第二种方案没有人使用,因为第一种方案存在的问题是:如果更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。
第二种方案存在的问题是:并发更新数据库场景下,会将脏数据刷到缓存
目前常用的是第三种和第四种方案。
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3vtuwevgbfms4