php网站建设是什么意思,去网站做dnf代练要押金吗,类似建设通的网站,口碑营销成功案例有哪些Redis底层原理
持久化
Redis虽然是个内存数据库#xff0c;但是Redis支持RDB和AOF #xff08;Redis Database Backup file#xff08;Redis数据备份文件#xff09;#xff0c;也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中 #xff1b;Appen…Redis底层原理
持久化
Redis虽然是个内存数据库但是Redis支持RDB和AOF Redis Database Backup fileRedis数据备份文件也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中 Append Only File追加文件两种持久化机制将数据写往磁盘可以有效地避免因进程退出造成的数据丢失问题当下次重启时利用之前持久化的文件即可实现数据恢复。
RDB
RDB持久化是把当前进程数据生成快照保存到硬盘的过程。所谓内存快照就是指内存中的数据在某一个时刻的状态记录。这就类似于照片当你给朋友拍照时一张照片就能把朋友一瞬间的形象完全记下来。RDB 就是Redis DataBase 的缩写。
给哪些内存数据做快照?
Redis 的数据都在内存中为了提供所有数据的可靠性保证它执行的是全量快照也就是说把内存中的所有数据都记录到磁盘中。但是RDB 文件就越大往磁盘上写数据的时间开销就越大。
RDB文件的生成是否会阻塞主线程
Redis 提供了两个手动命令来生成 RDB 文件分别是 save 和 bgsave。
save在主线程中执行会导致阻塞对于内存比较大的实例会造成长时间阻塞线上环境不建议使用。 bgsave创建一个子进程专门用于写入 RDB 文件避免了主线程的阻塞这也是Redis RDB 文件生成的默认配置。
命令实战演示 除了执行命令手动触发之外Redis内部还存在自动触发RDB 的持久化机制例如以下场景:
1)使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时自动触发bgsave。 2如果从节点执行全量复制操作主节点自动执行bgsave生成RDB文件并发送给从节点。
3)执行debug reload命令重新加载Redis 时也会自动触发save操作。 4默认情况下执行shutdown命令时如果没有开启AOF持久化功能则自动执行bgsave。 关闭RDB持久化在课程讲述的Redis版本6.2.4上是将配置文件中的save配置改为 save “” bgsave执的行流程
为了快照而暂停写操作肯定是不能接受的。所以这个时候Redis 就会借助操作系统提供的写时复制技术Copy-On-Write, COW在执行快照的同时正常处理写操作。 bgsave 子进程是由主线程 fork 生成的可以共享主线程的所有内存数据。bgsave 子进程运行后开始读取主线程的内存数据并把它们写入 RDB 文件。
如果主线程对这些数据也都是读操作例如图中的键值对 A那么主线程和bgsave 子进程相互不影响。但是如果主线程要修改一块数据例如图中的键值对 B那么这块数据就会被复制一份生成该数据的副本。然后bgsave 子进程会把这个副本数据写入 RDB 文件而在这个过程中主线程仍然可以直接修改原来的数据。
这既保证了快照的完整性也允许主线程同时对数据进行修改避免了对正常业务的影响。
RDB文件
RDB文件保存在dir配置指定的目录下文件名通过dbfilename配置指定。 可以通过执行config set dir {newDir}和config set dbfilename (newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。 Redis默认采用LZF算法对生成的RDB文件做压缩处理压缩后的文件远远小于内存大小默认开启可以通过参数config set rdbcompression { yes |no}动态修改。 虽然压缩RDB会消耗CPU但可大幅降低文件的体积方便保存到硬盘或通过网维示络发送给从节点,因此线上建议开启。 如果 Redis加载损坏的RDB文件时拒绝启动,并打印如下日志:
Short read or OOM loading DB. Unrecoverable erroraborting now.
这时可以使用Redis提供的redis-check-rdb工具(老版本是redis-check-dump)检测RDB文件并获取对应的错误报告。 RDB的优缺点
RDB的优点
RDB是一个紧凑压缩的二进制文件代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景。
比如每隔几小时执行bgsave备份并把 RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。
Redis加载RDB恢复数据远远快于AOF的方式。
RDB的缺点
RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
RDB文件使用特定二进制格式保存Redis版本演进过程中有多个格式的RDB版本存在老版本Redis服务无法兼容新版RDB格式的问题。
Redis中RDB导致的数据丢失问题
针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。
如下图所示我们先在 T0 时刻做了一次快照下一次快照是T4时刻然后在T1时刻数据块 5 和 8 被修改了。如果在T2时刻机器宕机了那么只能按照 T0 时刻的快照进行恢复。此时数据块 5 和 8 的修改值因为没有快照记录就无法恢复了。 所以这里可以看出如果想丢失较少的数据那么T4-T0就要尽可能的小但是如果频繁地执行全量 快照也会带来两方面的开销
1、频繁将全量数据写入磁盘会给磁盘带来很大压力多个快照竞争有限的磁盘带宽前一个快照还没有做完后一个又开始做了容易造成恶性循环。
2、另一方面bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然子进程在创建后不会再阻塞主线程但是fork 这个创建过程本身会阻塞主线程而且主线程的内存越大阻塞时间越长。如果频繁fork出bgsave 子进程这就会频繁阻塞主线程了。
所以基于这种情况我们就需要AOF的持久化机制。
AOF
AOF(append only file)持久化:以独立日志的方式记录每次写命令重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。理解掌握好AOF持久化机制对我们兼顾数据安全性和性能非常有帮助。
使用AOF
开启AOF功能需要设置配置:appendonly yes默认不开启。 AOF文件名通过appendfilename配置设置默认文件名是appendonly.aof。保存路径同RDB持久化方式一致通过dir配置指定。 AOF的工作流程
AOF的工作流程主要是4个部分:命令写入( append)、文件同步( sync)、文件重写(rewrite)、重启加载( load)。 命令写入
AOF命令写入的内容直接是RESP文本协议格式。例如lpush lijin A B这条命令在AOF缓冲区会追加如下文本:
*3\r\n$6\r\nlupush\r\n$5\r\nlijin\r\n$3\r\nA B
看看 AOF 日志的内容。其中“*3”表示当前命令有三个部分每部分都是由“$数字”开头后面紧跟着 具体的命令、键或值。这里“数字”表示这部分中的命令、键或值一共有多少字节。例如“$3 set”表示这部分有 3 个字节也就是“set”命令。
1 )AOF为什么直接采用文本协议格式?
文本协议具有很好的兼容性。开启AOF后所有写入命令都包含追加操作直接采用协议格式避免了二次处理开销。文本协议具有可读性,方便直接修改和处理。
2AOF为什么把命令追加到aof_buf中?
Redis使用单线程响应命令如果每次写AOF文件命令都直接追加到硬盘那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中还有另一个好处Redis可以提供多种缓冲区同步硬盘的策略在性能和安全性方面做出平衡。
Redis提供了多种AOF缓冲区同步文件策略由参数appendfsync控制。 always
同步写回每个写命令执行完立马同步地将日志写回磁盘
everysec
每秒写回每个写命令执行完只是先把日志写到 AOF 文件的内存缓冲区每隔一秒把缓冲区中的内容写入磁盘
no
操作系统控制的写回每个写命令执行完只是先把日志写到 AOF 文件的内存缓冲区由操作系统决定何时将缓冲区内容写回磁盘通常同步周期最长30秒。
很明显配置为always时每次写入都要同步AOF文件在一般的SATA 硬盘上Redis只能支持大约几百TPS写入,显然跟Redis高性能特性背道而驰,不建议配置。
配置为no由于操作系统每次同步AOF文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。
配置为everysec是建议的同步策略也是默认配置做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失1秒的数据。(严格来说最多丢失1秒数据是不准确的)
想要获得高性能就选择 no 策略如果想要得到高可靠性保证就选择always 策略如果允许数据有一点丢失又希望性能别受太大影响的话那么就选择everysec 策略。
重写机制
随着命令不断写入AOF文件会越来越大为了解决这个问题Redis引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
重写后的AOF 文件为什么可以变小?有如下原因:
1)进程内已经超时的数据不再写入文件。
2)旧的AOF文件含有无效命令如set a 111、set a 222等。重写使用进程内数据直接生成这样新的AOF文件只保留最终数据的写入命令。 3多条写命令可以合并为一个如:lpush list a、lpush list b、lpush list c可以转化为: lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出对于list、set、hash、zset等类型操作以64个元素为界拆分为多条。
AOF重写降低了文件占用空间除此之外另一个目的是:更小的AOF文件可以更快地被Redis加载。
AOF重写过程可以手动触发和自动触发:
手动触发:直接调用bgrewriteaof命令。 自动触发:根据auto-aof-rewrite-min-size和 auto-aof-rewrite-percentage参数确定自动触发时机。 auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积默认为64MB。
auto-aof-rewrite-percentage :代表当前AOF 文件空间(aof_currentsize和上一次重写后AOF 文件空间(aof_base_size)的比值。
另外如果在Redis在进行AOF重写时有写入操作这个操作也会被写到重写日志的缓冲区。这样重写日志也不会丢失最新的操作。
重启加载
AOF和 RDB 文件都可以用于服务器重启时的数据恢复。redis重启时加载AOF与RDB的顺序是怎么样的呢 1当AOF和RDB文件同时存在时优先加载AOF
2若关闭了AOF加载RDB文件
3加载AOF/RDB成功redis重启成功
4AOF/RDB存在错误启动失败打印错误信息
文件校验
加载损坏的AOF 文件时会拒绝启动对于错误格式的AOF文件先进行备份然后采用redis-check-aof --fix命令进行修复对比数据的差异找出丢失的数据有些可以人工修改补全。
AOF文件可能存在结尾不完整的情况比如机器突然掉电导致AOF尾部文件命令写入不全。Redis为我们提供了aof-load-truncated 配置来兼容这种情况默认开启。加载AOF时当遇到此问题时会忽略并继续启动,同时如下警告日志。 优缺点(持久化策略的选择的参考) ### RDB数据量小并发量小的情况下可以选择
- 优点
- RDB 是一个非常紧凑compact的文件体积小因此在传输速度上比较快因此适合灾难恢复。- RDB 可以最大化Redis 的性能父进程在保存RDB 文件时唯一要做的就是fork出一个子进程然后这个子进程就会处理接下来的所有保存工作父进程无须执行任何磁盘I/O 操作。- RDB 在恢复大数据集时的速度比AOF 的恢复速度要快。
- 缺点
RDB是一个快照过程无法完整的保存所有数据尤其在数据量比较大时候一旦出现故障丢失的数据将更多。
当redis中数据集比较大时候RDB由于RDB方式需要对数据进行完成拷贝并生成快照文件fork的子进程会耗CPU并且数据越大RDB快照生成会越耗时。
RDB文件是特定的格式阅读性差由于格式固定可能存在不兼容情况。
### AOF并发量大的情况下可以选择
- 优点- 数据更完整秒级数据丢失(取决于设置fsync策略)。- 兼容性较高由于是基于redis通讯协议而形成的命令追加方式无论何种版本的redis都兼容再者aof文件是明文的可阅读性较好。
- 缺点- 数据文件体积较大即使有重写机制但是在相同的数据集情况下AOF文件通常比RDB文件大。- 相对RDB方式AOF速度慢于RDB并且在数据量大时候恢复速度AOF速度也是慢于RDB。- 由于频繁地将命令同步到文件中AOF持久化对性能的影响相对RDB较大。
### 混合持久化4.0版本以后的默认选择方式
- 优点
混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式加载速度快同时结合AOF增量的数据以AOF方式保存了减少数据丢失。
- 缺点
兼容性差一旦开启了混合持久化在4.0之前版本都不识别该aof文件同时由于前部分是RDB格式阅读性较差。 redis淘汰策略持久化后完整流程下应该关注怎么淘汰
参考文章
redis六种淘汰策略redis默认的淘汰策略如何设置redis淘汰策略和最大内存
(noeviction(默认策略)对于写请求不再提供服务直接返回错误DEL请求和部分特殊请求除外)
redis六种淘汰策略redis默认的淘汰策略如何设置redis淘汰策略和最大内存_嗑嗑嗑瓜子的猫的博客-CSDN博客_redis默认淘汰策略
redis淘汰策略
redis淘汰策略_我们一直在路上的博客-CSDN博客_redis淘汰策略 RDB-AOF混合持久化
通过 aof-use-rdb-preamble 配置项可以打开混合开关yes则表示开启no表示禁用默认是禁用的可通过config set修改 该状态开启后如果执行bgrewriteaof命令则会把当前内存中已有的数据弄成二进程存放在aof文件中这个过程模拟了rdb生成的过程然后Redis后面有其他命令在触发下次重写之前依然采用AOF追加的方式 Redis持久化相关的问题
主线程、子进程和后台线程的联系与区别 进程和线程的区别
从操作系统的角度来看进程一般是指资源分配单元例如一个进程拥有自己的堆、栈、虚存空间页表、文件描述符等
而线程一般是指 CPU 进行调度和执行的实体。
一个进程启动后没有再创建额外的线程那么这样的进程一般称为主进程或主线程。
Redis 启动以后本身就是一个进程它会接收客户端发送的请求并处理读写操作请求。而且接收请求和处理请求操作是 Redis 的主要工作Redis 没有再依赖于其他线程所以我一般把完成这个主要工作的 Redis 进程称为主进程或主线程。
主线程与子进程
通过fork创建的子进程一般和主线程会共用同一片内存区域所以上面就需要使用到写时复制技术确保安全。
后台线程
从 4.0 版本开始Redis 也开始使用pthread_create 创建线程这些线程在创建后一般会自行执行一些任务例如执行异步删除任务
Redis持久化过程中有没有其他潜在的阻塞风险
当Redis做RDB或AOF重写时一个必不可少的操作就是执行fork操作创建子进程,对于大多数操作系统来说fork是个重量级错误。虽然fork创建的子进程不需要拷贝父进程的物理内存空间但是会复制父进程的空间内存页表。例如对于10GB的Redis进程需要复制大约20MB的内存页表因此fork操作耗时跟进程总内存量息息相关如果使用虚拟化技术特别是Xen虚拟机,fork操作会更耗时。
fork耗时问题定位:
对于高流量的Redis实例OPS可达5万以上如果fork操作耗时在秒级别将拖慢Redis几万条命令执行对线上应用延迟影响非常明显。正常情况下fork耗时应该是每GB消耗20毫秒左右。可以在info stats统计中查latest_fork_usec指标获取最近一次fork操作耗时,单位微秒。 如何改善fork操作的耗时:
1优先使用物理机或者高效支持fork操作的虚拟化技术
2控制Redis实例最大可用内存fork耗时跟内存量成正比,线上建议每个Redis实例内存控制在10GB 以内。
3降低fork操作的频率如适度放宽AOF自动触发时机避免不必要的全量复制等。 持久化机制问题参考文档
redis持久化机制_redis默认的持久化方式_CaptainCats的博客-CSDN博客
为什么主从库间的复制不使用 AOF
1、RDB 文件是二进制文件无论是要把 RDB 写入磁盘还是要通过网络传输 RDBIO效率都比记录和传输 AOF 的高。
2、在从库端进行恢复时用 RDB 的恢复效率要高于用 AOF。
分布式锁
Redis分布式锁最简单的实现
想要实现分布式锁必须要求 Redis 有「互斥」的能力我们可以使用 SETNX 命令这个命令表示SET if Not Exists即如果 key 不存在才会设置它的值否则什么也不做。
两个客户端进程可以执行这个命令达到互斥就可以实现一个分布式锁。
客户端 1 申请加锁加锁成功
客户端 2 申请加锁因为它后到达加锁失败 此时加锁成功的客户端就可以去操作「共享资源」例如修改 MySQL 的某一行数据或者调用一个 API 请求。
操作完成后还要及时释放锁给后来者让出操作共享资源的机会。如何释放锁呢
也很简单直接使用 DEL 命令删除这个 key 即可这个逻辑非常简单。 但是它存在一个很大的问题当客户端 1 拿到锁后如果发生下面的场景就会造成「死锁」
1、程序处理业务逻辑异常没及时释放锁
2、进程挂了没机会释放锁
这时这个客户端就会一直占用这个锁而其它客户端就「永远」拿不到这把锁了。怎么解决这个问题呢
如何避免死锁
我们很容易想到的方案是在申请锁时给这把锁设置一个「租期」。
在 Redis 中实现时就是给这个 key 设置一个「过期时间」。这里我们假设操作共享资源的时间不会超过 10s那么在加锁时给这个 key 设置 10s 过期即可
SETNX lock 1 // 加锁
EXPIRE lock 10 // 10s后自动过期 这样一来无论客户端是否异常这个锁都可以在 10s 后被「自动释放」其它客户端依旧可以拿到锁。
但现在还是有问题
现在的操作加锁、设置过期是 2 条命令有没有可能只执行了第一条第二条却「来不及」执行的情况发生呢例如 SETNX 执行成功执行EXPIRE 时由于网络问题执行失败 SETNX 执行成功Redis 异常宕机EXPIRE 没有机会执行 SETNX 执行成功客户端异常崩溃EXPIRE也没有机会执行
总之这两条命令不能保证是原子操作一起成功就有潜在的风险导致过期时间设置失败依旧发生「死锁」问题。
在 Redis 2.6.12 之后Redis 扩展了 SET 命令的参数用这一条命令就可以了
SET lock 1 EX 10 NX 锁被别人释放怎么办
上面的命令执行时每个客户端在释放锁时都是「无脑」操作并没有检查这把锁是否还「归自己持有」所以就会发生释放别人锁的风险这样的解锁流程很不「严谨」如何解决这个问题呢
解决办法是客户端在加锁时设置一个只有自己知道的「唯一标识」进去。
例如可以是自己的线程 ID也可以是一个 UUID随机且唯一这里我们以UUID 举例
SET lock $uuid EX 20 NX
之后在释放锁时要先判断这把锁是否还归自己持有伪代码可以这么写
if redis.get(lock) $uuid:redis.del(lock)
这里释放锁使用的是 GET DEL 两条命令这时又会遇到我们前面讲的原子性问题了。这里可以使用lua脚本来解决。
安全释放锁的 Lua 脚本如下
if redis.call(GET,KEYS[1]) ARGV[1]
thenreturn redis.call(DEL,KEYS[1])
elsereturn 0
end
好了这样一路优化整个的加锁、解锁的流程就更「严谨」了。
这里我们先小结一下基于 Redis 实现的分布式锁一个严谨的的流程如下
1、加锁
SET lock_key $unique_id EX $expire_time NX
2、操作共享资源
3、释放锁Lua 脚本先 GET 判断锁是否归属自己再DEL 释放锁
Java代码实现分布式锁
package com.msb.redis.lock;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;/*** 分布式锁的实现*/
Component
public class RedisDistLock implements Lock {private final static int LOCK_TIME 5*1000;private final static String RS_DISTLOCK_NS tdln:;/*if redis.call(get,KEYS[1])ARGV[1] thenreturn redis.call(del, KEYS[1])else return 0 end*/private final static String RELEASE_LOCK_LUA if redis.call(get,KEYS[1])ARGV[1] then\n return redis.call(del, KEYS[1])\n else return 0 end;/*保存每个线程的独有的ID值*/private ThreadLocalString lockerId new ThreadLocal();/*解决锁的重入*/private Thread ownerThread;private String lockName lock;Autowiredprivate JedisPool jedisPool;public String getLockName() {return lockName;}public void setLockName(String lockName) {this.lockName lockName;}public Thread getOwnerThread() {return ownerThread;}public void setOwnerThread(Thread ownerThread) {this.ownerThread ownerThread;}Overridepublic void lock() {while(!tryLock()){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}Overridepublic void lockInterruptibly() throws InterruptedException {throw new UnsupportedOperationException(不支持可中断获取锁);}Overridepublic boolean tryLock() {Thread t Thread.currentThread();if(ownerThreadt){/*说明本线程持有锁*/return true;}else if(ownerThread!null){/*本进程里有其他线程持有分布式锁*/return false;}Jedis jedis null;try {String id UUID.randomUUID().toString();SetParams params new SetParams();params.px(LOCK_TIME);params.nx();synchronized (this){/*线程们本地抢锁*/if((ownerThreadnull)OK.equals(jedis.set(RS_DISTLOCK_NSlockName,id,params))){lockerId.set(id);setOwnerThread(t);return true;}else{return false;}}} catch (Exception e) {throw new RuntimeException(分布式锁尝试加锁失败);} finally {jedis.close();}}Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {throw new UnsupportedOperationException(不支持等待尝试获取锁);}Overridepublic void unlock() {if(ownerThread!Thread.currentThread()) {throw new RuntimeException(试图释放无所有权的锁);}Jedis jedis null;try {jedis jedisPool.getResource();Long result (Long)jedis.eval(RELEASE_LOCK_LUA,Arrays.asList(RS_DISTLOCK_NSlockName),Arrays.asList(lockerId.get()));if(result.longValue()!0L){System.out.println(Redis上的锁已释放);}else{System.out.println(Redis上的锁释放失败);}} catch (Exception e) {throw new RuntimeException(释放锁失败,e);} finally {if(jedis!null) jedis.close();lockerId.remove();setOwnerThread(null);System.out.println(本地锁所有权已释放);}}Overridepublic Condition newCondition() {throw new UnsupportedOperationException(不支持等待通知操作);}}锁过期时间不好评估怎么办 看上面这张图加入key的失效时间是10s但是客户端C在拿到分布式锁之后然后业务逻辑执行超过10s那么问题来了在客户端C释放锁之前其实这把锁已经失效了那么客户端A和客户端B都可以去拿锁这样就已经失去了分布式锁的功能了
比较简单的妥协方案是尽量「冗余」过期时间降低锁提前过期的概率但是这个并不能完美解决问题那怎么办呢
分布式锁加入看门狗
加锁时先设置一个过期时间然后我们开启一个「守护线程」定时去检测这个锁的失效时间如果锁快要过期了操作共享资源还未完成那么就自动对锁进行「续期」重新设置过期时间。
这个守护线程我们一般也把它叫做「看门狗」线程。
为什么要使用守护线程 分布式锁加入看门狗代码实现 运行效果 Redisson中的分布式锁
Redisson把这些工作都封装好了 dependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.12.3/version/dependency
package com.msb.redis.config;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;Configuration
public class MyRedissonConfig {/*** 所有对Redisson的使用都是通过RedissonClient*/Bean(destroyMethodshutdown)public RedissonClient redisson(){//1、创建配置Config config new Config();config.useSingleServer().setAddress(redis://127.0.0.1:6379);//2、根据Config创建出RedissonClient实例RedissonClient redisson Redisson.create(config);return redisson;}
}package com.msb.redis.redisbase.adv;import com.msb.redis.lock.rdl.RedisDistLockWithDog;
import org.junit.jupiter.api.Test;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;SpringBootTest
public class TestRedissionLock {private int count 0;Autowiredprivate RedissonClient redisson;Testpublic void testLockWithDog() throws InterruptedException {int clientCount 3;RLock lock redisson.getLock(RD-lock);CountDownLatch countDownLatch new CountDownLatch(clientCount);ExecutorService executorService Executors.newFixedThreadPool(clientCount);for (int i 0;iclientCount;i){executorService.execute(() - {try {lock.lock(10, TimeUnit.SECONDS);System.out.println(Thread.currentThread().getName()准备进行累加。);Thread.sleep(2000);count;} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}countDownLatch.countDown();});}countDownLatch.await();System.out.println(count);}
}GitHub - redisson/redisson: Redisson - Redis Java client with features of In-Memory Data Grid. Over 50 Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Publish / Subscribe, Bloom filter, Spring Cache, Tomcat, Scheduler, JCache API, Hibernate, MyBatis, RPC, local cache ...
Redisson: Redis Java client with features of In-Memory Data Grid
锁过期时间不好评估怎么办
集群下的锁还安全么
基于 Redis 的实现分布式锁前面遇到的问题以及对应的解决方案
1、死锁设置过期时间
2、过期时间评估不好锁提前过期守护线程自动续期
3、锁被别人释放锁写入唯一标识释放锁先检查标识再释放
之前分析的场景都是锁在「单个」Redis实例中可能产生的问题并没有涉及到 Redis 的部署架构细节。
而我们在使用 Redis 时一般会采用主从集群 哨兵的模式部署这样做的好处在于当主库异常宕机时哨兵可以实现「故障自动切换」把从库提升为主库继续提供服务以此保证可用性。
但是因为主从复制是异步的那么就不可避免会发生的锁数据丢失问题加了锁却没来得及同步过来。从库被哨兵提升为新主库这个锁在新的主库上丢失了
Redlock真的安全吗
Redis 作者提出的 Redlock方案是如何解决主从切换后锁失效问题的。
Redlock 的方案基于一个前提
不再需要部署从库和哨兵实例只部署主库但主库要部署多个官方推荐至少 5 个实例。
注意不是部署 Redis Cluster就是部署 5 个简单的 Redis 实例。它们之间没有任何关系都是一个个孤立的实例。
做完之后我们看官网代码怎么去用的
8. 分布式锁和同步器 · redisson/redisson Wiki · GitHub
8.4. 红锁RedLock
基于Redis的Redisson红锁 RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个 RLock对象关联为一个红锁每个 RLock对象实例可以来自于不同的Redisson实例。
RLock lock1 redissonInstance1.getLock(lock1);
RLock lock2 redissonInstance2.getLock(lock2);
RLock lock3 redissonInstance3.getLock(lock3);
RedissonRedLock lock new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
大家都知道如果负责储存某些分布式锁的某些Redis节点宕机以后而且这些锁正好处于锁住的状态时这些锁会出现锁死的状态。为了避免这种情况的发生Redisson内部提供了一个监控锁的看门狗它的作用是在Redisson实例被关闭前不断的延长锁的有效期。默认情况下看门狗的检查锁的超时时间是30秒钟也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了 leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RedissonRedLock lock new RedissonRedLock(lock1, lock2, lock3);
// 给lock1lock2lock3加锁如果没有手动解开的话10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间并在加锁成功10秒钟后自动解开
boolean res lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
Redlock实现整体流程
1、客户端先获取「当前时间戳T1」
2、客户端依次向这 5 个 Redis 实例发起加锁请求
3、如果客户端从 3 个大多数以上Redis 实例加锁成功则再次获取「当前时间戳T2」如果 T2 - T1 锁的过期时间此时认为客户端加锁成功否则认为加锁失败。
4、加锁成功去操作共享资源
5、加锁失败/释放锁向「全部节点」发起释放锁请求。
所以总的来说客户端在多个 Redis 实例上申请加锁必须保证大多数节点加锁成功大多数节点加锁的总耗时要小于锁设置的过期时间释放锁要向全部节点发起释放锁请求。
我们来看 Redlock 为什么要这么做 为什么要在多个实例上加锁
本质上是为了「容错」部分实例异常宕机剩余的实例加锁成功整个锁服务依旧可用。 为什么大多数加锁成功才算成功
多个 Redis 实例一起来用其实就组成了一个「分布式系统」。在分布式系统中总会出现「异常节点」所以在谈论分布式系统问题时需要考虑异常节点达到多少个也依旧不会影响整个系统的「正确性」。
这是一个分布式系统「容错」问题这个问题的结论是如果只存在「故障」节点只要大多数节点正常那么整个系统依旧是可以提供正确服务的。 为什么步骤 3 加锁成功后还要计算加锁的累计耗时
因为操作的是多个节点所以耗时肯定会比操作单个实例耗时更久而且因为是网络请求网络情况是复杂的有可能存在延迟、丢包、超时等情况发生网络请求越多异常发生的概率就越大。
所以即使大多数节点加锁成功但如果加锁的累计耗时已经「超过」了锁的过期时间那此时有些实例上的锁可能已经失效了这个锁就没有意义了。 为什么释放锁要操作所有节点
在某一个 Redis 节点加锁时可能因为「网络原因」导致加锁失败。
例如客户端在一个 Redis 实例上加锁成功但在读取响应结果时网络问题导致读取失败那这把锁其实已经在 Redis 上加锁成功了。
所以释放锁时不管之前有没有加锁成功需要释放「所有节点」的锁以保证清理节点上「残留」的锁。
好了明白了 Redlock 的流程和相关问题看似Redlock 确实解决了 Redis 节点异常宕机锁失效的问题保证了锁的「安全性」。
但事实真的如此吗
RedLock的是是非非
一个分布式系统更像一个复杂的「野兽」存在着你想不到的各种异常情况。
这些异常场景主要包括三大块这也是分布式系统会遇到的三座大山NPC。
NNetwork Delay网络延迟
PProcess Pause进程暂停GC
CClock Drift时钟漂移
比如一个进程暂停GC的例子 1客户端 1 请求锁定节点 A、B、C、D、E
2客户端 1 的拿到锁后进入 GC时间比较久
3所有 Redis 节点上的锁都过期了
4客户端 2 获取到了 A、B、C、D、E 上的锁
5客户端 1 GC 结束认为成功获取锁
6客户端 2 也认为获取到了锁发生「冲突」
GC 和网络延迟问题这两点可以在红锁实现流程的第3步来解决这个问题。
但是最核心的还是时钟漂移因为时钟漂移就有可能导致第3步的判断本身就是一个BUG所以当多个 Redis 节点「时钟」发生问题时也会导致 Redlock 锁失效。
RedLock总结
Redlock 只有建立在「时钟正确」的前提下才能正常工作如果你可以保证这个前提那么可以拿来使用。
但是时钟偏移在现实中是存在的
第一从硬件角度来说时钟发生偏移是时有发生无法避免。例如CPU 温度、机器负载、芯片材料都是有可能导致时钟发生偏移的。
第二人为错误也是很难完全避免的。
所以Redlock尽量不用它而且它的性能不如单机版 Redis部署成本也高优先考虑使用主从 哨兵的模式 实现分布式锁只会有很小的记录发生主从切换时的锁丢失问题。