目录
4. 如果在上锁之后,设置过期时间之前,服务器异常,就无法设置过期时间,可以在上锁的同时设置过期时间。
1 Redis脑裂
1.1 概念
1. 假设现在有三台机器,分别安装了redis服务,结构如图
2.
如果此时
master
服务器所在区域网络通信出现异常,导致和两台
slave
机器无法正常通信,但是和 客户端的连接是正常的。那么sentinel
就会从两台
slave
机器中选举其中一个作为新的
master
来处
理客户端请求。
3.
这个时候,已经存在两台
master
服务器,
client
发送的数据会持续保存在旧的
master
服务器中,而新的master
和
slave
中没有新的数据。如果一分钟以后,网络恢复正常,服务之间能够正常通信。 此时,sentinel
会把旧的
master
会变成新的
master
的
slave
节点。
4.
问题出现了,我们都知道,
slave
会从
master
中同步数据,保持主从数据一致。这个时候,变成了 slave节点的旧
master
会丢失掉通信异常期间从客户端接收到的数据。
1.2 解决方案
第一个参数表示最少的
slave
节点为
1
个
第二个参数表示数据复制和同步的延迟不能超过
10
秒
配置了这两个参数:如果发生脑裂:原
master
会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。
2 缓存预热
新启动的系统没有任何缓存数据,在缓存重建数据的过程中,系统性能和数据库负载都不太好,所以最 好是在系统上线之前就把要缓存的热点数据加载到缓存中,这种缓存预加载手段就是缓存预热。
3 缓存穿透
1 概念
如果某个
key
对应的数据不存在,而又未对该
key
做缓存,所以每次请求都会穿过缓存直接到数据库进行 查询,并发量高的情况下进而导致数据库直接宕机,这就是缓存穿透。
2 解决方案
1.
对空值缓存:如果一个查询返回的数据为空(不管数据是否存在),我们仍然把这个空结果缓存, 设置空结果的过期时间会很短,最长不超过5
分钟。
2.
设置白名单:使用
bitmaps
类型定义一个可以访问的名单,用户
id
作为偏移量,每次访问查询是否 在白名单中,如果不存在,则拒绝访问。
3.
布隆过滤器:类似一个
hash set
,用来判断某个元素(
key
)是否在某个集合中。
和一般的
hash set
不同的是,这个算法无需存储
key
的值,对于每个
key
,只需要
k
个比特位,每个
存储一个标志,用来判断
key
是否在集合中。
4 缓存击穿
1 概念
某一个热点
key
,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最 终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。
2 解决方案
1.
加互斥锁:在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线 程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,其他线程直接查询缓存。
2.
热点数据不过期:直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。
5 缓存雪崩
1 概念
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发 到DB
,
DB
瞬时压力过重雪崩。
和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不 到从而查数据库。
2 解决方案
1.
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2.
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
3.
设置热点数据永远不过期。
6 分布式锁
随着业务发展的需要,原单机部署的系统被演化成分布式集群系统,由于分布式系统多线程、多进程且分布在不同的机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API
并不能提供分 布式锁的能力。为了解决这个问题需要一种跨JVM
的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁主流的实现方案:
1.
基于数据库实现分布式锁
2.
基于缓存(
Redis
等)
3.
基于
Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
1.
性能:
Redis
最高
2.
可靠性:
Zookeeper
最高
1 设置锁和过期时间(redis)
1. 通过setnx上锁
由于
setnx
只有不存在该
key
的时候,可以设置成功,并返回
1
,否则设置失败,并返回
0
。
2. 通过del释放锁
3. 如果锁一直不释放,需要增加过期时间,防止资源浪费。
4. 如果在上锁之后,设置过期时间之前,服务器异常,就无法设置过期时间,可以在上锁的同时设置过期时间。
2 防止误删
避免误删情况出现,可以在加锁过程中添加一个加锁的唯一
id
,通过跟该
id
对比,阻止误删的情况出现。
3 保证删除原子性
Lua
脚本
Lua
是一种轻量小巧的脚本语言,用标准
C
语言编写并以源代码形式开放, 其设计目的是为了
嵌入应用
程序
中,从而为应用程序提供灵活的扩展和定制功能。
Redis
中引入
lua
的优势:
减少网络开销:多个请求通过脚本一次发送,减少网络延迟
原子操作
:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
复用:客户端发送的脚本永久存在
redis
中,其他客户端可以复用脚本
可嵌入性:可嵌入
JAVA
,
C#
等多种编程语言,支持不同操作系统跨平台交互
lua
进行比较
uuid
,对比成功后删除键值对的代码:
if
redis.call
(
'get'
,
KEYS
[
1
]) ==
ARGV
[
1
]
then
return
redis.call
(
'del'
,
KEYS
[
1
])
else
return
0
end
if
中的比较如果是
true ,
那么 执行
del
并返回
del
结果;如果
if
结果为
false
直接返回
0
。
其中的
KEYS[1] , ARGV[1] 是参数
,我们只调用
jedis
执行脚本的时候,传递这两个参数就可以了。 通过jedis
执行
lua
脚本
7 消息队列
1 List消息队列
List
底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是
O(1)
,这意味着它非常符合消息队列的模型。
生产者使用 lpush发布消息
消费者这一侧,使用 rpop拉取消息:
一般编写消费者逻辑时,通过一个
“
死循环
”
实现,如果此时队列为空,那消费者依旧会频繁拉取消息,造成资源浪费。
Redis 提供「阻塞式」拉取消息的命令:brpop / blpop,这里的 B 指的是阻塞(Block)。
brpop key timeout
:移除并返回最后一个值,同时需要传入一个超时时间(
timeout
),如果设置为 0,则表示不设置超时,直到有新消息才返回,否则会在指定的超时时间后返回
NULL
。
缺点:
不支持重复消费:消费者拉取消息后,这条消息就从
List
中删除了,无法被其它消费者再次消费,
即不支持多个消费者消费同一批数据
消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了
8 发布/订阅消息队列
Redis
提供了
PUBLISH / SUBSCRIBE
命令,来完成发布、订阅的操作。
多个消费者,同时消费同一批数据
通过生产者,发布一条消息。
客户端接收到消息
使用
Pub/Sub
这种方案,既支持阻塞式拉取消息,还很好地满足了多组消费者,消费同一批数据的业务需求。
但是该方案会引起
消息丢失
:
消费者下线
Redis
宕机
消费者:
通过
jedis
订阅频道,需要一个
JedisPubSub
子类对象,并重写
onMessage
方法用于接受消息
8 数据一致性解决方案
读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现
缓存
(Redis)
和数据库(
MySQL
)间的数据一致性问题
。
不管是先写
MySQL
数据库,再删除
Redis
缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。
例:
1.
如果删除了缓存
Redis
,还没有来得及写库
MySQL
,另一个线程就来读取,发现缓存为空,则去数 据库中读取数据写入缓存,此时缓存中为脏数据。
2.
如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情
况。 因为写和读是并发的,没法保证顺序,
就会出现缓存和数据库的数据不一致的问题
1 延时双删策略
1.
先删除缓存。
2.
再写数据库。
3.
休眠
500
毫秒;
4.
再次删除缓存。
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然这种策略还要考虑
redis
和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms
即可。比如:休眠
1
秒。
缺点:
结合双删策略
+
缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
9 企业级持久化解决方案
在企业中不要仅仅使用
RDB
,因为那样会导致丢失很多数据。
也不要仅仅使用
AOF
,因为那样有两个问题:
1.
通过
AOF
做冷备,没有
RDB
做冷备,来的恢复速度更快;
2. RDB
每次简单粗暴生成数据快照,更加健壮,可以避免
AOF
这种复杂的备份和恢复机制的
bug
。
综合使用
AOF
和
RDB
两种持久化机制,用
AOF
来保证数据不丢失,作为数据恢复的第一选择;
用
RDB
来做不同程度的冷备,在
AOF
文件都丢失或损坏不可用的时候,还可以使用
RDB
来进行快速的数据恢复。
如果
RDB
在执行
snapshotting
操作,那么
redis
不会执行
AOF rewrite
;如果
redis
再执行
AOF rewrite
, 那么就不会执行RDB snapshotting
。
如果
RDB
在执行
snapshotting
,此时用户执行
BGREWRITEAOF
命令,那么等
RDB
快照生成之后,才会去执行AOF rewrite
。
1 RDB的生成策略
如果希望能确保
RDB
最多丢
1
分钟的数据,那么尽量就是每隔
1
分钟都生成一个快照。不过到底是
10000 条执行一次RDB
,还是
1000
条执行一次
RDB
,这个根据需要根据自己的应用和业务的数据量来确定。
2 AOF的生成策略
AOF一定要打开,fsync方式选择everysec。一般可能会调整的参数可能就是下面俩参数了
auto-aof-rewrite-percentage 100
就是当前
AOF
大小膨胀到超过上次
100%
,上次的两倍。
auto-aof-rewrite-min-size 64mb
根据自己的数据量来定,
16mb
,
32mb
。
3 企业级的数据备份方案
RDB
非常适合做冷备,每次生成之后,就不会再有修改。
数据备份方案:
1.
写定时调度脚本去做数据备份。
2.
每小时都
copy
一份
rdb
的备份,到一个目录中去,仅仅保留最近
48
小时的备份。
3.
每天都保留一份当日的
rdb
的备份,到一个目录中去,仅仅保留最近
1
个月的备份。
4.
每次
copy
备份的时候,都把太旧的备份给删了。
5.
每天晚上将当前服务器上所有的数据备份,发送一份到远程的云服务上去。