• 取得连接,会使用到 mysql 中的连接器
    • 还会经历 tcp 三次握手,因为 mysql 是基于 tcp 进行通信的
    • 这个过程会校验用户名和密码是否正确
    • 校验用户权限
  • 查询缓存,key 为 sql 语句,value 为 sql 查询结果,不过在 mysql8.0 后被删掉了
    • 更新比较频繁的表缓存命中率很低,因为只要一个表被更新了,该表的缓存就会被删掉,维护起来就很麻烦
  • 分析器,分为词法分析和语法分析。词法分析就是提取 sql 语句关键字,语法分析就是校验 sql 语法,构建 sql 语法树,方便后面去读取表名,列名,语句类型
  • 执行阶段
    • 预处理
      • 检查 sql 查询语句中的表 or 字段是否存在
      • 把*扩展为表上的所有列
    • 优化阶段
      • 决定如何走索引
      • 如何 jion
    • 执行阶段:根据表的引擎定义,去执行这个引擎提供的接口

redis会把设置了过期时间的key放到一个过期字典里

定期:每隔一段时间从过期字典里随机取一批key判断是否过期,过期就“删除”

惰性:访问到已经过期的key时就直接删除

redis默认使用的是定期+惰性删除

redis默认每秒进行10次的过期检查,我们可以通过redis.config设置hz。随机抽取的数量是写死的

为20

且这个定期删除不会立马删,而是检测到某些key过期了,会把它放到一个List里,但redis的内存使用率

达到某个阈值后再统一删。

还有就是,被删除的过期key原来占用的空间并不会立马被操作系统回收,而是会标记为可重用的内存,

以此来提高性能。(毕竟向系统申请空间or回收是很耗性能的)

如何设置过期时间

expire :设置key在n秒后过期

pexpire :设置key在n毫秒后过期

expireat :设置key在n时间戳(精确到秒)后过期

pexpireat :设置key在n时间戳(精确到毫秒)后过期

或者在设置字符串时也可以一起设置:

set ex,set px

查看key的过期时间 ttl

只有内存满了才会执行

如果从淘汰的对象角度来说的话:

针对所有key

针对设置了ttl的key

如果从淘汰的算法角度来说

LRU

LFU

随机选一个删除

不删,直接返回错误信息

如何配置

通过redis的 maxmemory-policy参数来指定

如何选择?

参考过一些大厂的配置建议

比如腾讯

它是根据redis的使用情况来考虑的,

若你redis当缓存的话,就设置allKeys-lru。会把最近最少使用的key删掉

若你redis当半持久化or半缓存使用,可以使用volatile-lru

不过像腾讯云的redis云产品默认的是不删除

阿里云默认的是volatile-lru

LRU变种实现

Redis中的事务,multi表示开启一个事务(类似MySQL中的begin),然后执行一系列命令(放入队列中,提交后依次执行),exec提交(类似MySQL中的commit)

原子性:Redis中的事务不满足原子性,如果命令中有错误指令,如自增一个字符串的key,提交事务后,其它正常的语句仍能执行,事务不会回滚

事务中读请求没有意义,返回值用不了,因为此时命令还没真正执行,只是放到了一个队列中,exec提交后才真正执行;而MySQL开启事务后执行指令,那是真正实打实的执行的

既然不能在事务中读(没有意义),那就得在事务外面读,然后开启事务,再进行写操作,但这样无法保证读+写的原子性,可能读完后,事务提交前有其它连接更改了数据,解决方案:

  • watch命令,用来盯住一个到多个key,如果这些key在事务期间:

没有被其它客户端修改,exec能成功

被其它客户端修改,exec返回nil

例如:

1
2
3
4
5
6
watch a b // 监视a b 
get a // 返回1000
get b // 返回500
multi // 开启事务
set a 500, set b 1000 // a给b转账500
exec

如果第一条指令到最后一条指令期间,有其它客户端修改了a或b的值,exec不会提交事务,返回nil

  • lua脚本(2表示接下来的两个值是key,key后面的是参数,即a和b是key,500是参数)

为什么要上锁?锁的本质是什么?

上锁的本质其实就是为了让资源能被正常的修改。为什么不上锁就不行呢?因为在JMM中,存在

着共享内存以及线程私有内存的,线程不是总会把私有内存里的资源立马加载到共享的,存在着时间

差,也就导致了覆盖的问题,此时就是我们常说的线程不安全。这时候我们使用ReentrantLock or · synchronized去包住一段代码,保证 同一时间内只会有一个线程去操作里面的资源,即使不会立马同步

到共享内存,也不会有其他线程来竞争。而且只要代码执行出了锁的范围,就会立马同步到主存

这里的资源是指堆内的,即成员变量

可以看到synchronized和ReentrantLock锁的是比较大的范围,容易误伤

所以我们可以把锁的范围降低,同时并不总是一直会有 对共享资源的访问和修改,我们可以基于一种乐观的

思想,比较资源的现有值和预期值,如果一致,说明没有人访问过,那么我直接修改

预期值和现有值是指什么?

比如一个对象里有个字段int a,我之前读出来,读出a是2,这个2就是预期值,那么我需要把他加10,更改为12,此时要更改了,我就看他的现有的实际的值,如果还是预期值2,说明在我读出来,再做运算,再到现在打算更改这个过程中,没有其它线程来修改,那么我就可以把它改为12。如果不是2,例如变成5了,那么实际值5,就和预期值2不一样了, 说明这段时间内有其它线程对其修改了,那我就不能动他,否则就产生覆盖了,因为我的12是基于2来计算出来的

即 一起竞争,谁快谁有理

CAS compare And swap 正是这样的锁,其实不只是锁了,更能说是一种思想。但其实也能说是一种锁,毕竟比较和交换这两步得是原子的。

cas基于cpu的一个原子指令来使其原子操作

cmp x chg:cpu执行这条指令时,会自动锁住总线,防止其他cpu访问共享变量;cpu同时自动禁止中断

同时硬件会保证对共享变量的访问是原子的

CAS存在的问题:

ABA,但说实在的业务上根本没有这个问题

CAS与悲观锁的区别:

  • 粒度不同,CAS的粒度是针对一个变量的修改, 悲观锁的粒度是一段代码块
  • 思想不同,CAS是乐观的思想,失败了大不了再重试,悲观锁是悲观的思想,我就笃定会有其它线程干扰,直接上锁
  • 场景不同,CAS适用读多写少,悲观锁适用读少写多
  • 开销不同,CAS开销小,悲观锁开销大

有了CAS为什么还要volatile?

CAS只是原子修改,并不能保证可见性,修改完后,其它线程并不一定马上能看到最新值

AOF

ps:AOF的诞生是为了解决早期的RDB无法及时持久化的问题

redis执行写命令操作后,会把其命令追加到AOF文件里,AOF是文本格式文件,AOF持久化默认是默认关闭

AOF的刷盘时机有三种,具体是由config里的appendfsync 参数控制的

1.always:每执行一条写命令就写入一次(每次write,每次fsync)

2.Everysec:每秒执行追加到文件一次,会先将命令写到AOF对应的内核缓冲区,后每隔一秒再写入

磁盘中的文件(每次write,每秒fsync)

3.no:由操作系统来决定啥时候刷盘(每次write)

AOF重写

它这个就是说当AOF文件大小到达某些阈值后(可配置),会读取redis中的所有键值对,生成命令写入 新的AOF文件里,然后用新的替换旧的

目的:给文件瘦身,比如说一开始有set saki 69,set saki 91两个命令,重写后就只有set saki 91一条

命令了

为什么要用新的文件替换旧的文件,而不是直接覆盖呢

怕重写一般失败了,污染了旧文件

写入是在主线程执行的,因为写入的量不多。但重写是会fork子进程来完成的,这其中会把父进程的数据拷贝一份

….(待补充)

为了避免子进程在重写时,主进程的数据发生了变化,导致AOF文件里的数据和内存里的对应起来不一致。redis对其进行了一层优化,即将重写阶段新来的数据写入AOF的重写缓冲区里,等到AOF重写完成后,再把AOF重写缓冲区里的数据追加到新的AOF文件中。

RDB快照

RDB快照就是指记录某一时间段的内存数据,记录的是实际数据,因此在做内存恢复时效率是要比AOF高不

少的。

RDB可以直接读入内存就好了,不用像AOF那样需要执行命令

RDB是紧凑的二进制文件

RDB还提供了两种命令来生成RDB文件

save:主线程直接执行,会阻塞正常命令的执行

bgsave:操作系统fork子进程执行,可以避免主线程的阻塞

生成RDB时,不会保存过期的key

导入RDB时,主节点会对过期key进行过滤,而从不会

bgsave和AOF重写一致,都会fork子进程,但在bgsave期间新来的数据是不会被保存到此时的RDB的,只能等到下一次RDB

其他生成

正常停机时生成一次RDB

内部触发机制 比如save/bgsave 91 1:代表91秒内至少有1个key被修改,就RDB一次

redis默认的持久化策略:采用RDB

save 900 1

save 300 10

save 60 10000

appendonly no

注意:这个save不会阻塞主线程,**Redis 自动触发 ****bgsave** 来生成快照

4.x后的混合持久化

原理

就是开一个定时任务,,然后每隔10s检查一次,并将其续约恢复至我们设置的lockWatchdogTimeout(默

认是30s)

jvm挂了,看门狗会一直续期下去吗

挂了的话,不会一直续期的。看门狗是jvm线程,jvm挂了的话,会终止续期的

加锁线程挂了,但jvm没挂,看门狗会一直续期下去吗

看门狗线程本质就是一个守护线程,且这个线程是和加锁线程绑定的,或者说它续期会去判断加锁线程是否存

活,存活才会去续期

并非守护线程

源码解读

续期任务调度的实现

这个方法是为锁开启续期任务,且一把锁只会有一个续期任务!!!由第一个持有该锁的线程开启,后续第

n个持有该锁的线程只需要把threadId注册到一个concurrentHashMap里

为什么?

你续期任务本质就是一个开一个线程不断轮询,那一个线程配一个定时线程这显然是不合理的。肯定

是不合理的,只需要一把锁对应一个续期任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected void scheduleExpirationRenewal(long threadId) {
//new一个续期任务
ExpirationEntry entry = new ExpirationEntry();
//EXPIRATION_RENEWAL_MAP:concurrentHashMap k:锁 v:一个entry field有线程ID
//Q:为什么要为concurrentHashMap来保证线程安全呢
//A:因为它是Redisson所有锁的基类,像读写锁这种,可能会有多个线程共同持有锁的情况
//如果这个锁已经有一个续期任务在执行了,就不覆盖,返回旧的 entry;否则返回 null,表示可以放入
ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
//不为空,说明这把锁已经有续期任务了
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
//首次为这把锁开 续期任务 并把当前线程注册到
entry.addThreadId(threadId);

try {
//执行续期
this.renewExpiration();
} finally {
//如果线程被中断了 就取消续期
//Q:什么情况下,线程会被中断?
if (Thread.currentThread().isInterrupted()) {
this.cancelExpirationRenewal(threadId);
}

}
}

}

续期的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* Redisson 分布式锁 —— 看门狗续期核心逻辑
*
* 功能:
* 1. 定期(默认锁租期的 1/3)检查锁是否仍然被当前线程持有;
* 2. 若锁仍在,则延长过期时间(防止业务执行过长锁被自动释放);
* 3. 若锁已不存在,则取消续期任务;
* 4. 若发生异常(如 Redis 宕机),则清理当前锁的续期状态。
*
* 实现细节:
* - 使用 Netty 的时间轮(HashedWheelTimer)定时调度任务;
* - 使用异步命令(CompletionStage)执行 Redis 脚本;
* - 通过回调 whenComplete 处理续期结果或异常。
*/
private void renewExpiration() {
// 获取当前锁对应的续期任务信息(存放于全局 Map)
ExpirationEntry ee = (ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
// 使用 Netty 时间轮延迟执行任务(间隔 = 租期时间 / 3)
//newTimeout:
Timeout task = this.getServiceManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 再次确认该锁的续期任务仍存在(防止任务被取消)
ExpirationEntry ent = (ExpirationEntry) RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
if (ent != null) {
// 获取该锁第一个持有线程的 threadId(可能是重入锁)
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
/**
* 续期逻辑:
* 调用 Lua 脚本检测锁是否仍属于当前线程,
* 若是,则执行 pexpire 延长过期时间;
* 返回值:
* true -> 锁还在,续期成功;
* false -> 锁已不存在或非本线程持有。
*/
CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);

/**
* Q:为什么是异步?
* A:
* - renewExpiration() 是周期性执行的;
* - 如果同步等待 Redis 响应,Netty 时间轮线程会被阻塞;
* - 异步执行可以充分利用线程池,提高续期稳定性;
* - 防止 Redis 网络抖动造成时间轮延迟。
*/

// 注册异步回调:处理续期结果与异常
future.whenComplete((res, e) -> {
/**
* e:表示异步任务执行过程中发生的异常(如 Redis 连接错误、脚本超时等)
* res:表示 Redis 脚本返回结果(true=锁还在,false=锁不存在)
*/
if (e != null) {
// 出现异常,打印日志 + 清理续期任务(避免死循环)
RedissonBaseLock.log.error(
"Can't update lock {} expiration",
RedissonBaseLock.this.getRawName(), e
);
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
} else {
if (res) {
// res == true:锁仍被当前线程持有,继续下一次续期
RedissonBaseLock.this.renewExpiration();
} else {
// res == false:锁不存在(可能已释放或超时),停止续期任务
RedissonBaseLock.this.cancelExpirationRenewal(null);
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);

// 将定时任务句柄存回 ExpirationEntry(便于后续取消)
ee.setTimeout(task);
}
}

Redisson的定时任务是基于Netty中的时间轮来实现的

时间轮是什么?时间轮算法

Redisson执行定时任务的work线程

timer就是一个HashedWheelTimer(Netty的时间轮)

return 一个Timeout(表示你刚刚注册的这个定时任务,你可以通过这个timeout来查看任务状态等等)

newTimeout:往时间轮里面 注册一个定时任务

使用

两个常用API

tryLock:非阻塞锁,如果不设置waitTime的话,就直接return了

lock:阻塞锁,线程获取不到锁,会阻塞住

为什么基于lua脚本实现而不是事务?

两者都是可以保证原子性的,但

可重入锁?

redisson是基于redis的数据结构hash去实现可重入锁的。key为lock,field为线程名,value为count。这个

count就是用来表示一个锁被同一线程持有的情况

加锁流程:就是会先判断锁是否被当前线程持有或者说有没有线程持有,不是就表示加锁失败,是的话,就

count++;

解锁流程:每次执行完毕就count–。直到count==0,就把其key删除掉,表示锁完全释放

加锁流程和解锁流程都是基于lua脚本实现的

不止hash结构?

1.字符串拼接,把count拼进去

2.redis那里还是使用string,服务内部通过concurrentHashMap保存:key为线程唯一标识,value为锁

计数器

1
2
3
4
5
6
// 用于保存线程对应的锁和重入次数
//外层 String 是线程 ID(可以用 Thread.currentThread().getId() 拼上业务 ID),或 UUID。
//内层 Map:key 是 Redis 的锁 key,value 是重入次数。
private final ConcurrentHashMap<String, Map<String, Integer>> threadLockMap
= new ConcurrentHashMap<>();

可阻塞锁,可重试锁?

Redisson的可阻塞锁是基于Redis的发布订阅模式来实现等待,唤醒,获取锁失败的重试机制的

获取锁失败不是直接重试or返回,而是订阅一下,然后等待

当获取锁成功的线程释放锁后,就会发布一条消息

其他线程就会收到这条消息,从而重新获取锁,获取失败就会继续等待

但也不是无限等待,超过一定时间,就不会继续等了,而是会返回false(针对于tryLock())

联锁?

对于分布式锁在主从架构中的锁丢失问题,Redisson提供了一种联锁机制。它要求Redis使用多主多从或者多

主,那么只有所有Redis主节点都上锁成功,才算上锁成功。这样的话,如果某个主节点宕机了,那么其他主

节点也是有锁的数据的,新线程想要给所有主节点加锁,那还是会加锁失败的

ps:主从架构中的锁丢失问题:当主节点setnx成功后,这时候来没来得及同步就挂了。那么这时候就会重新

选主,但这时候新的主节点是没锁数据的,服务器就会认为锁已经释放了,导致锁的互斥性失效了。

红锁?

但对于联锁而言,还可能会存在以下问题

1.所有主节点都得上锁,若某个主节点由于网络原因,导致加锁时间长,加锁失败

2.或者某个主节点宕机了,一直加锁失败

那么如果出现以上情况,就会导致一直加锁失败,失败概率很高,而且加锁失败后是要回滚所有Redis主节点

的数据的,性能很差的。

那么基于以上问题………………

Redis官方提供了一种红锁机制,也要求Redis要多主部署,但加锁时只要半数以上加锁成功就OK了。如果

当前线程加锁加到半数以上,那么其他线程就不可能加锁加到了半数以上了,那么这样就满足了互斥性

但redLock也是有很大问题的,

主要原因是Redis创始人不推荐在严格一致性的分布式情况下使用它

有什么可以替代的吗?业界无公认的方案

我个人的思考

1.使用单实例的Redis锁,虽然会有单节点故障

2.业务做好唯一性校验

3.使用强一致性组件来实现分布式锁,如zookeeper

分布式锁检验死锁?

观察看门狗线程,如果出现某两个看门狗线程存活时间过长,则这两个看门狗对应的分布式锁可能产生死锁