0%

数据库笔记

mysql 事务 索引 锁 隔离 mvcc sql优化

脏读不可重复读幻读

redis 底层 如底层数据结构、RESP协议、持久化机制、过期键处理及内存淘汰机制等。

Mysql

mysql的数据是存在磁盘上的而不是内存,mysql中的具体数据是存储中行中的,而行是存储在页中的。页是InnoDB存储引擎磁盘管理的最小单位,默认一页大小为16K

事务 索引 b+树 锁 sql调优 开放性问题

mysql 底层引擎 innodb 页大小16kb 快照mvcc

innoDB 使用 B+Tree 作为数据结构,在索引和数据存储方面的结构:

innoDB 使用 B+Tree 作为数据存储结构, 采用聚簇索引方式,数据行存放在叶子节点中,并通过链表指针形式进行连接。 显然一个表只存在一个聚簇索引,即以该表的主键为索引key, 值为存放在叶子节点的数据行。 其它索引都是非聚簇索引(辅助索引), 查找的都是数据主键Id, 根据id 回表查询得到最终数据行。

页 是InnoDB存储引擎管理数据库的最小磁盘单位,一个页的大小一般是16KB。一次至少读取一页的数据到内存,或者刷新一页的数据到磁盘。

在操作系统中,内存与磁盘交换,是以页为单位的,一页大小4KB。同样的在MySQL中为了提高吞吐率,数据也是分页的,不过MySQL的数据页大小是16KB。(确切的说是InnoDB数据页大小16KB)。

Mysql 里的锁种类,作用,功能 并发中mvcc和锁

https://zhuanlan.zhihu.com/p/149228460

范围粒度划分 InnoDB行级锁

表级锁 mysql server 实现

MySQL表锁是MySQL数据库管理系统中的一种锁定机制,用于锁定整张表的数据。

而MySQL Server则是MySQL数据库管理系统的服务端程序,它是数据库系统的核心部分,负责管理数据库的存储,处理客户端的请求并执行SQL语句。

因此,MySQL表锁是在MySQL Server中实现的,并且与MySQL Server紧密关联。用户可以通过向MySQL Server发送请求,实现对整张表的数据进行锁定,以保证数据的一致性和事务隔离。

InnoDB行级锁

行级锁 innoDB 主要使用 记录所 间隙锁 Next-Key锁 意向锁

记录锁 : 给单行记录加上锁,防止查询过程中记录被修改,防止其他事务干扰当前事务正在操作的数据。

间隙锁 : 给查询范围内连续的空间间隙加上锁,防止事务过程中其他事务插入数据到该空间中,造成幻读。

临键锁 : 记录所+间隙锁 给单行记录和查询范围加上锁, 防止查询过程范围内的记录被修改和插入新的记录,造成可重复读、幻读

间隙锁(Gap Locks) 和 临键锁(Next-Key Locks) 都是用来解决幻读问题的,
在 已提交读(READ COMMITTED) 隔离级别下, 间隙锁(Gap Locks) 和 临键锁
(Next-Key Locks) 都会失效!

InnoDB意向锁 / 插入意向锁 :一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了意向锁 ,如果有的话,插入操作需要等待,直到拥有 gap锁 的那个事务提交。但是事务在等待的时候也需要在内存中生成一个 锁结构 ,表明有事务想在某个 间隙 中插入新记录,但是现在在等待。

页级锁:

开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般

功能划分

  1. 读锁 共享锁

当前事务对数据被加上S锁后,可以允许其他事务可以加上S锁。

若其他事务对该数据要加上X锁,则会将该事务挂起,等待前面的事务提交完成,释放S锁。

  1. 写锁 互斥锁 排他锁

X锁排他,拒绝其他事务对该数据获取任何锁,执行事务提交释放锁。

  1. 意向锁
    在执行锁获取前,先获取意向锁,若符合条件则获取锁成功,若不符合获取锁的条件则将事务挂起,等待其他锁释放,再执行上锁操作。

获取S锁,若无锁或S锁状态,则获取S锁成功; 若X锁状态,则事务挂起进入阻塞状态,等待锁释放。

获取X锁,必须等待到所有锁释放

性质划分

  1. 悲观锁

悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能被改动,一个事务
拿到悲观锁后,其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以
执行。
数据库中的行锁,表锁,读锁,写锁均为悲观锁。

  1. 乐观锁 无锁并发 CAS原子操作机制

乐观锁认为数据的变动不会太频繁。
乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,
版本最为常用。不同于悲观锁,乐观锁通常是由开发者实现的。

mvcc 和 锁

MVCC(多版本并发控制)和锁机制都是MySQL中用于实现并发控制的机制。它们的使用时机如下:

MVCC 适用于读多写少的场景,比如OLTP(联机事务处理)系统。在MVCC中,读操作不会阻塞写操作,写操作不会阻塞读操作。MVCC 使用版本号来实现读写之间的隔离,不需要加锁,因此对于读操作来说,性能很高。但对于写操作,MVCC 在数据版本升级和回收过程中会产生额外的开销。

锁机制适用于写多读少的场景,比如OLAP(联机分析处理)系统。在锁机制中,读写之间会互相阻塞,因此在写操作过程中,读操作无法执行。锁机制使用的锁可以是共享锁和排它锁,共享锁可以同时由多个事务持有,用于读操作;而排它锁只能由一个事务持有,用于写操作。锁机制的优点是对数据的修改可以保证原子性,但是对于读操作来说,可能会因为等待锁而导致性能下降。

mvcc 具体 cas 版本号 每个事务都有一个版本号,读写能并发

锁 同一时间只能读或者写
mvcc 同一时间可以读写并发 版本链 undolog Read View(读视图)

mvcc 快照读

每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

根据数据的版本号,判断该数据版本是在本事务开启前还是开启后, 如果是开启前, 正常读取。
如果是开启后,则通过该数据的undo logs 的指针,回滚查找到本事务的版本号的历史数据,作为快照读。

事务的每次操作都要加上版本号 undolog 记录,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作

Read View(读视图)快照

ReadView是事务开启时,当前所有活跃事务(还未提交的事务)的一个集合,ReadView数据结构决定了不同事务隔离级别下,数据的可见性。

查询readview中的数据时,当前事务版本id 与 数据的版本id比较,
这里举例论证了可见性判断的合理性,总结来说,Read View(读视图)可见性的三个判断约束了一件事,只有在本事务生成Read View之前就已经提交的事务的修改才可以被看见,其他的无论是正在进行的事务的修改还是之后再提交的事务的修改都不可见。

一个事务在对一行数据做读取操作的时候,会从undo log历史版本链中从最新版本开始往前比对,通过一系列的规则,只有在本事务生成Read View之前就已经提交的事务的修改才可以被看见,其他的无论是正在进行的事务的修改还是之后再提交的事务的修改都不可见,直到找到可见版本或找不到任何一个可见版本

根据快照版本中的trx_id字段和read view来确定该版本对于当前事务是否可见,如果当前比对版本不可见,那么就通过roll_pointer找到上一个版本进行比对,直到找到可见版本或找不到任何一个可见版本

事务 隔离

https://developer.aliyun.com/article/743691

事务特性

事务特性ACID

原子性 一致性 隔离性 持久性

脏读 不可重复读 幻读

  1. 脏读(Dirty Read)
    一个事务读到了另一个未提交事务修改过的数据

会话B开启一个事务,把id=1的name为武汉市修改成温州市,此时另外一个会话A也开启一个事务,读取id=1的name,此时的查询结果为温州市,会话B的事务最后回滚了刚才修改的记录,这样会话A读到的数据是不存在的,这个现象就是脏读。(脏读只在读未提交隔离级别才会出现)

  1. 不可重复读(Non-Repeatable Read)
    一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。(不可重复读在读未提交和读已提交隔离级别都可能会出现)

会话A开启一个事务,查询id=1的结果,此时查询的结果name为武汉市。接着会话B把id=1的name修改为温州市(隐式事务,因为此时的autocommit为1,每条SQL语句执行完自动提交),此时会话A的事务再一次查询id=1的结果,读取的结果name为温州市。会话B再此修改id=1的name为杭州市,会话A的事务再次查询id=1,结果name的值为杭州市,这种现象就是不可重复读。

  1. 幻读(Phantom)
    一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。(幻读在读未提交、读已提交、可重复读隔离级别都可能会出现)

会话A开启一个事务,查询id>0的记录,此时会查到name=武汉市的记录。接着会话B插入一条name=温州市的数据(隐式事务,因为此时的autocommit为1,每条SQL语句执行完自动提交),这时会话A的事务再以刚才的查询条件(id>0)再一次查询,此时会出现两条记录(name为武汉市和温州市的记录),这种现象就是幻读。

事务 commit 与 修改数据

MySQL 中的事务可以进行多次提交。在一个事务内,可以多次执行修改操作,最后通过 COMMIT 语句将修改提交到数据库。每次 COMMIT 语句都是一次提交。因此,在一个事务内可以进行任意多次提交,只要事务没有被 ROLLBACK 或者被自动回滚。

注意,不建议在一个事务内多次提交,因为多次提交会增加数据库的负担,降低效率。通常情况下,一个事务只需要进行一次提交。

隔离级别 读未提交 读已提交 可重复读 可串行化

MySQL 事务的隔离机制是一组数据库管理系统的规则,用于确定多个并发访问和更新数据的事务如何隔离。这是通过对并发事务的访问和更新进行限制,以避免数据不一致的情况。MySQL 支持四种事务隔离级别:

多个事务同时操作(读取修改)同一数据的隔离级别,

  1. 读未提交 (READ UNCOMMITTED):允许事务未提交情况下,读取其他事务修改但未提交的数据。这是最低的隔离级别,这可能导致脏读、不可重复读和幻读的情况。

  2. 读已提交 (READ COMMITTED):允许事务在未提交情况下,读取其他事务修改并且已经提交的数据。但是在事务提交期间,可能会出现不可重复读和幻读的情况。

  3. 可重复读 (REPEATABLE READ):允许事务在修改数据并且提交后,才能读取其他事务已经修改并提交的数据。 事务读取某数据后,直到事务提交再释放锁

该隔离级别保证了一个事务对同一数据的多次读取都得到相同的结果,防止了不可重复读和幻读。但是在并发事务对相同数据进行修改时,仍然可能出现幻读的情况。

  1. 串行化 (SERIALIZABLE):这是最高的隔离级别,在串行化隔离级别下,所有的读写操作都会加上排它锁,因此可以完全防止并发问题。保证了事务的串行执行,防止了脏读、不可重复读和幻读的情况。

默认情况下,MySQL 使用 REPEATABLE READ 隔离级别。可以使用 SET TRANSACTION ISOLATION LEVEL 语句来更改隔离级别。

隔离级别的实现

通过锁实现了读未提交和串行化;

通过锁配合MVCC实现了读已提交和可重复读。

  1. 锁机制
  • 读未提交:读数据时不加锁;可以读取其他事务修改为提交的数据
  • 读已提交:读数据时加读锁(共享锁),但是读完就释放锁而不是等到事务结束; 可以读取其他事务修改并提交的数据
  • 可重复读:读数据时加读锁(共享行锁),事务结束释放锁; 事务读取某数据后,直到事务提交再释放锁
  • 可串行化:读数据时添加共享表锁,事务结束释放锁。
  1. MVCC机制(MySQL主要使用快照保证隔离级别)

MVCC机制生成一个数据快照,并用这个快照来提供一定级别的一致性的读取,也成为了多版本数据控制。
MVCC实现的是普通读取不加锁,并且读写分离,保证了读写并发。

· 实际就是CAS版本控制和读写分离
· 主要作用读已提交和可重复读

当前读:读取的是记录的最新版本,需要保证其它事务不能修改读取记录,所以会对记录进行加锁。比如 select for update、select lock in share mode、update等,都属于当前读。
快照读:基于MVCC实现的读,不对读操作加任何锁,读取的时候根据版本链和read view进行可见性判断,所以读取的数据不一定是数据库中的最新值。注意在串行化隔离级别下,读操作也会加锁,所以属于当前读。 根据mvcc的版本号,通过undolog将表恢复到事务开始执行的状态,然后再完成事务的执行,解决幻读的问题。

索引

作用:

在表中建立索引,可以提高对表数据的查询效率

传统的查询方法,是按照表的顺序遍历的,不论查询几条数据,MySQL需要将表的数据从头到尾遍历一遍。

底层B+Tree 生成一个索引文件,在查询数据库时,找到索引文件进行遍历,在比较小的索引数据里查找,然后映射到对应的数据,能大幅提升查找的效率

分类:

使用分类:
主键索引(默认)
唯一索引
普通索引
组合索引

索引和数据(叶子节点)是否在同一棵树上储存
聚簇索引(主键索引) 索引值ID+数据 数据存储方式 索引的逻辑顺序与数据的物理顺序一致
非聚簇索引(二级索引) 索引值+主键ID -> 根据ID回表 索引的逻辑顺序与数据的物理顺序不一致

聚簇索引与主键索引的关系

覆盖索引 :所要查询的值是辅助索引的值,直接返回值,不用回表查主键索引值。

索引的适用条件(or not):

索引(a,b,c)where b=xx and a=yy and c =zz走不走索引为啥?
走索引,可以去看看mysql的优化器,底层内部有启发式的优化算法,sql语句写的顺序并没有任何影响

B+TREE 优缺点比较 二叉树 B树 HASH

最左匹配

左边遇到范围查询,停止后面的匹配

在InnoDB的联合索引中,查询的时候只有匹配了前一个/左边的值之后,才能匹配下一个。

慢查询 查询优化 分库分表 主从库读写分离 主从复制 动静分离

分库分表

  1. 分库:单库来处理所有事务,高并发时有性能瓶颈,可以分出一个副数据库,将不经常查询的表放到里面,分开查询。但是实现复杂,客户端系统需要多一个mysql连接。

  2. 分表:

  • 经常查询的大表拆分小表,经常查询的字段放一起,不经常查询的字段(如大文本text)分开存放在另一个表。
  • 不经常查询修改的大文本text,独立存放。

redis

https://zhuanlan.zhihu.com/p/487583440

底层实现 String list set sortedset hash

Redis 的底层实现是使用 C 语言编写的,采用了非阻塞 I/O 和事件驱动模型。它使用内存存储数据,在内存中操作数据,并使用持久化技术(例如 RDB 和 AOF)来确保数据在不正常关闭的情况下能够恢复。

Redis 实现了多种数据类型,如字符串、列表、集合、有序集合和哈希表,以及许多高级特性,例如事务、订阅/发布、分布式锁和 Lua 脚本。

在底层实现中,Redis 使用了字典(dictionary)数据结构来存储键值对,并使用哈希表(hash table)来实现查询操作的高效性。在哈希表上,Redis 使用了自适应哈希(Adaptive Hash)算法来维护哈希表的负载平衡和冲突解决。

Redis 的高性能和高可用性主要归功于其简单的数据模型、非阻塞 I/O、事件驱动架构和使用内存存储数据的策略。

String

sds simply dynamic string 简单动态字符串

struct sdshdr {
// 用于记录buf数组中使用的字节的数目和SDS存储的字符串的长度相等
int len;
// 用于记录buf数组中没有使用的字节的数目
int free;
// 字节数组,用于储存字符串
char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的。
};

  1. len 可以直接查出字符串长度,复杂度O(1);如果用c语言字符串,查询字符串长度需要遍历整个字符串,复杂度为O(n);

为什么要自己定义SDS, 不用C字符串?

  1. 获取字符串长度的复杂度为O(N) ,获取字符串长度的复杂度为O(1)

  2. API是不安全的,可能会造成缓冲区溢出。 API是安全的,不会造成缓冲区溢出

  3. 修改字符串长度N次必然需要执行N次内存重分配, 修改字符串长度N次最多需要执行N次内存重分配

  4. 只能保存文本数据 , 可以保存文本或者二进制数据

  5. 可以使用所有库中的函数, 可以使用一部分库的函数

List

Set

SortedSet

Hash

SkipList 跳表

https://www.cnblogs.com/Elliott-Su-Faith-change-our-life/p/7545940.html
o(logn)

持久化机制

Redis 支持两种方式的数据持久化:RDB 和 AOF。

RDB文件存储数据, AOF文件存储操作日志

RDB:它是通过定时将内存中的数据快照保存到磁盘上来实现持久化,每隔一段时间,Redis 会自动将当前内存数据备份到一个 RDB 文件中,并在数据发生损坏的情况下使用该文件进行数据恢复。

AOF:它是通过将 Redis 执行的每个写命令记录到 AOF 文件中来实现持久化,在 Redis 启动时,它会读取 AOF 文件并重放其中的命令,从而恢复到持久化之前的状态。

用户可以根据自己的实际需要使用这两种方式中的任意一种或者同时使用这两种方式进行数据持久化。

redis 内存回收、内存淘汰

Redis内存回收机制
Redis的内存回收主要围绕以下两个方面:
1.Redis过期策略
删除过期时间的key值
2.Redis淘汰策略
内存使用到达maxmemory上限时触发内存淘汰数据
Redis的过期策略和内存淘汰策略不是一件事,实际研发中不要弄混淆了,下面会完整的介绍两者。

Redis内存过期回收策略

过期策略通常有以下三种:

  1. 定时过期
    每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

  2. 惰性过期 lazy 访问到过期再清除
    只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

  3. 定期过期
    每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
    Redis中同时使用了惰性过期和定期过期两种过期策略。

Redis内存淘汰策略 maxmemory maxmeory-samples

  1. 简介
    Redis的内存淘汰策略,是指当内存使用达到maxmemory极限时,需要使用LAU淘汰算法来决定清理掉哪些数据,以保证新数据的存入。

  2. LRU算法
    Redis默认情况下就是使用LRU策略算法。
    LRU算法(least RecentlyUsed),最近最少使用算法,也就是说默认删除最近最少使用的键。
    但是一定要注意一点!redis中并不会准确的删除所有键中最近最少使用的键,而是随机抽取3个键,删除这三个键中最近最少使用的键。
    那么3这个数字也是可以可以设置采样的大小,如果设置为10,那么效果会更好,不过也会耗费更多的CPU资源。对应位置是配置文件中的maxmeory-samples。

  3. 缓存清理配置
    maxmemory用来设置redis存放数据的最大的内存大小,一旦超出这个内存大小之后,就会立即使用LRU算法清理掉部分数据。
    对于64 bit的机器,如果maxmemory设置为0,那么就默认不限制内存的使用,直到耗尽机器中所有的内存为止;,但是对于32 bit的机器,有一个隐式的闲置就是3GB

  4. Redis数据淘汰策略
    maxmemory-policy,可以设置内存达到最大闲置后,采取什么策略来处理。
    对应的淘汰策略规则如下:

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,在抽取的部分过期key移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,在抽取的部分过期较久的key优先移除。
  1. 缓存清理的流程
    客户端执行数据写入操作 redis server接收到写入操作之后,检查maxmemory的限制,如果超过了限制,那么就根据对应的policy清理掉部分数据,写入操作完成执行。

总结

redis的内存淘汰策略用于处理内存不足时的需要申请额外空间的数据,内存淘汰策略的选取并不会影响过期的key的处理。过期策略用于处理过期的缓存数据。

resp协议

分布式 缓存一致性 分布式锁 消息队列 微服务 raft

缓存一致性

CAP理论

严格意义上任何非原子操作都不可能保证一致性,除非用阻塞读写实现强一致性,所以缓存架构我们追求的目标是最终一致性。
缓存就是通过牺牲强一致性来提高性能的。

这是由CAP理论决定的。缓存系统适用的场景就是非强一致性的场景,它属于CAP中的AP。

实现策略

https://www.cnblogs.com/xiaolincoding/p/16493675.html

  1. 发布/订阅(Publish/Subscribe):当数据存储中的数据发生更改时,发布通知,以通知所有关注该数据的缓存更新其缓存。

  2. 直接写入数据库,再删除缓存。
    Cache Aside(旁路缓存)策略是一种缓存一致性策略。
    读取数据:从缓存读取数据,读到直接返回。如果读取不到的话,从数据库加载,写入缓存后,再返回响应。
    写入数据:更新的时候,先更新数据库,然后再删除缓存。

Cache Aside策略是一种简单且常用的缓存一致性策略,适用于大多数场景。它使用起来简单,并且可以很好地利用缓存的优势,提高读取数据的效率。但是,当数据在数据存储中被更新时,Cache Aside策略不能自动更新缓存中的数据,因此需要进行手动更新。

为什么先修改数据库再删除缓存, 而不是删除缓存再修改数据库?

因此操作缓存的速度比操作数据库快。

先修改数据库再删除缓存,数据库与缓存不一致的时间只是缓存删除的时间。

先删除缓存再修改数据库,数据库与缓存不一致的时间是修改数据库的时间。

  1. 直接修改缓存, 同步写入数据库。
    “读穿” (Read-through) 策略是先在缓存中检查读操作的数据,如果数据在缓存中找到,则从缓存返回,否则从后端存储中获取并存储在缓存中,以备后续读取。这种方法可以提高读取密集型操作的性能,因为经常访问的数据可以从缓存返回,而不是从后端存储中返回,从而获得更快的访问时间。

“写穿” (Write-through) 策略是立即将所有向缓存的写操作写入后端存储,以确保数据在缓存和后端存储之间始终保持同步。这种方法适用于数据一致性是最重要考虑因素的系统,例如数据库系统,因为它可以确保所有数据修改对所有访问数据的进程都是立即可见的。

优势在于,它能够保证数据的一致性,因为当数据被写入到缓存中时,它同时也被写入到主存储器中,从而避免了数据丢失的风险。
然而,Write-through cache也有一些缺点。因为每次写入操作都需要同时写入到缓存和主存储器中,因此它的写入速度要比其他缓存策略慢。此外,如果在写入操作期间出现故障,可能导致数据的不一致。

总的来说,写穿缓存提供了数据的一致性和可靠性,而读穿缓存提高了读操作的性能。在缓存系统中,两种策略可以共同使用,以实现一致性和性能的双重优势。

  1. 直接修改缓存,标记为脏,异步批量更新时写回数据库。若宕机,有数据丢失风险。
    “写回” (Write-back) 策略是将写操作的数据先写入缓存,然后再异步地写回后端存储。如果在写回后端存储之前,缓存数据发生更改,则缓存将记录这些更改,并在将数据写回后端存储时将这些更改一并写回。这种策略可以提高缓存的写性能,因为写入操作的数据不需要立即写入后端存储,但可能带来一致性问题,因为如果在写回后端存储之前缓存数据丢失,则所有未写回后端存储的修改将丢失。

“读回” (Read-back) 策略是将缓存中的数据定期与后端存储中的数据进行比较,如果数据发生变化,则将缓存中的数据更新为最新的数据。这种策略可以保证缓存数据的一致性,因为缓存数据定期与后端存储中的数据进行同步,但可能会降低缓存的读性能,因为需要定期向后端存储请求数据。

raft 分布式集群状态一致性 日志 节点请求广播

https://juejin.cn/post/6998470783831900197

Raft 协议是一种用于分布式系统中的一致性算法,主要用于维护集群中多个节点的状态一致性。实现 Raft 协议的主要步骤如下:

领导人选举:Raft 协议使用了一种简单的领导人选举机制,其中所有节点通过发送请求来竞选为领导者,最后选举出的节点成为领导者。

日志复制:领导者通过发送 AppendEntries 消息来复制它的日志到其他节点。

投票请求:当领导者失去联系时,其他节点将通过发送请求投票请求来选举一个新的领导者。

响应客户端请求:当领导者收到客户端的请求时,它将将请求添加到它的日志中,然后将该请求广播到其他节点,以确保它们的状态保持一致。

整个 Raft 协议的实现需要考虑到许多细节,例如网络分区的情况,状态转换的规则以及如何处理冲突。有关 Raft 协议的详细信息,可以参考相关的论文和技术文档。

节点及其状态: 领导者 跟随者 候选者

Raft 定义了三种角色:领导者(Leader)、跟随者(Follower) 和 候选人(Candidate)。这三种状态不是一成不变的,每个节点都会在这三种状态中进行转换。

  1. 领导者:当节点作为领导者时,所有集群接收的数据都将通过他接收,再由他决定对数据的处理。他将和所有其他节点保持一个心跳连接(心跳数据包),以维护自己的领导状态。心跳连接通过发送一个个的心跳包实现,如果要对其他节点发出命令,就会将命令和数据携带在心跳包中。

  2. 跟随者:当节点作为跟随者时,只会负责执行领导者的决策,或者响应领导者发送过来的心跳包,以显示自己仍处于连接。

  3. 候选人:当发现领导者下线之后,跟随者节点会马上转变为候选人,并开始组织竞选,通过拉票的方式竞选成为新一任的领导者。

节点有两个必须属性:

currentTerm:节点当前所处的任期。
votedFor  :节点当前任期所跟随的其他节点的 ID。如果是投票阶段,表示节点所投的候选人。如果已经竞选结束,就是当前的领导人。

超时时间

领导者定时发送心跳包给跟随者, 跟随者在超过这个时间 (选举超时时间) 没有收到心跳包会认为领导者已下线,进入候选者状态向其他节点发送请求投票给自己,在 (投票超时时间) 内获得半数以上即当选,得票不过半数即该节点选举失败,等一段时间之后 (竞选等待超时时间) 再参与竞选。

  1. 选举超时时间:

领导者定时发送心跳包给跟随者, 跟随者在超过这个时间没有收到心跳包会认为领导者已下线,进入候选者状态向其他节点发送请求投票给自己,获得半数以上即当选。

  1. 投票超时时间: 候选者邀请其他节点为自己投票,超过投票超时节点得票不过半数即该节点选举失败,等一段时间之后再参与竞选。

当跟随者变成候选人时,会开启投票超时的倒计时,并邀请所有其他节点为自己投票。在倒计时结束之前如果得票超一半节点数,候选人就竞选成功。

如果到了投票超时时间还没攒够票数,该候选人就会宣告这一轮竞选失败,会等一段时间之后再参与竞选。

其他节点响应投票邀请时,只会回复是否投票给发起者,所有发起投票的候选人是无法得知其他人的得票情况的,只能统计自己的得票数。因此候选人失败时是不知道其他候选人是否成功的,甚至不知道其他候选人的存在。

  1. 竞选等待超时时间: 当候选人选举失败时,会等待一段时间之后再次参与竞选,这段等待时间就是竞选等待超时时间。

此时其他节点可以参加竞选,也可能已经在竞选了,如果候选人在等待投票超时时间或者竞选等待超时时间时其他节点竞选成功,则候选人会马上转变为跟随者跟随该新晋的领导者。

  1. 超时时间重置

只要跟随者收到请求,就会重置自身的选举超时时间,因此领导者会不断地周期性地发送心跳包控制跟随者。同时候选人的投票邀请也会重置跟随者的请求超时时间,让收到投票但是还没参加竞选的跟随者不参与竞选。

请求: 投票请求 数据追加请求(日志)

节点能发送数据追加请求表明是领导者, 因此节点收到数据追加请求表面对方是领导者(根据两者term比较,判断 term 任期是否合法, 来接收数据请求)。

  1. 数据追加请求
    当领导者接收到客户端的数据,就会发送数据追加请求将数据复制给所有的跟随者,因此保证所有节点的数据统一。当传输的数据为空,该请求就变成了保持连接的心跳包。

  2. 投票请求
    当候选人发起投票时,会将投票请求发现所有其他节点,请求其他节点进行投票。每个接收投票请求的节点都会返回自己的投票结果。

任期概念 term votedFor

Raft 定义了 任期(Term) 这一概念,所有节点都会有 currentTerm 这个属性,就是该节点当前所处的任期。
任期在 Raft 算法里起一个逻辑时钟的作用,第几个任期,即第几个 term 就类似于我国的第几个朝代这种概念,而领导人,就仿佛是朝代的君王。

  1. 当节点收到 term 比自己大的请求, 无论当前节点处于何种状态都会将自己的votedFor跟随指向该节点

无论是投票请求还是数据追加请求,只要请求中的 term 大于节点自身的任期,就表明对方比自身要更先进,此时节点无条件跟随对方,将自己的 votedFor 设置为对方的 ID,并更新自身任期为 term。

  • 如果是投票请求,表示对方先于自己发现领导人已下线和先于其他人来拉票,根据这个信息从节点的角度出发该候选人有比较大的概览胜选,因此跟随它并给他投票。
  • 如果是数据追加请求,表明对方是最新任期的竞选获胜者,同时表示节点自身没有参加该任期的选举,可能是节点临时掉线或投票请求丢包。此时直接跟随领导人即可。
  1. 任何节点收到任期等于自身任期的数据追加请求时,需要马上跟随对方。 对方任期等于节点自身任期,并且能够发送数据追加请求(心跳包),就表明对方是领导者。

而我们知道,一轮选举只能选出一位领导者,因此该发送方必定是此轮选举获胜者,因此,如果节点的 votedFor 不是它,表示当前节点可能是获选候选人的跟随者或者落选候选人本人,此时需要马上将自身的 votedFor 设置为对方的 ID 来跟随他。

  1. 任何节点收到任期等于自身任期的投票请求时, 不会改变。 因为,此时 term 发生变化时, votedFor已经决定了要么是自己要么是先接收到其他候选节点的投票请求。

此时再收到term一样的投票时, 它 已经投过票了 或者 节点是候选者。

votedFor 在任期发生更新时就确定了,要么是自己,要么是第一个来拉票的候选人。只有在自己或者跟随的候选人选举失败时,才会发生更改来跟随获胜的领导人,否则一直都不会变。

  1. 如果收到任期比自身小的请求直接丢弃,否则必须回复。

如果请求的任期比节点自身任期小,无论是数据追加请求还是投票请求,都表明对方已经有一段时间没有和其他节点沟通了,应该是掉线了的领导者或者竞选者。此时对方的状态是过时的,一切请求都直接丢弃。

单线程 多线程

为什么早期单线程?后期多线程?
因为Redis是基于内存的操作,瓶颈在内存性能上,多线程只是提高cpu利用率,并不能提升redis的性能。

因为Redis是基于内存的操作,CPU成为Redis的瓶颈的情况很少见,Redis的瓶颈最有可能是内存的大小或者网络限制。
Redis 4.0 之后开始变成多线程,除了主线程外,它也有后台
线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 Key 的删
除等等。

为什么Redis 6.0 引入多线程
Redis 6.0中的多线程,也只是针对处理网络请求过程采用了多线程,而数据的读写命令,仍然是单线程处理的。

虽然现在很多服务器都是多个CPU核的,但是对于Redis来说,因为使用了单线程,在一次数据操作的过程中,有大量的CPU时间片是耗费在了网络IO的同步处理上的,并没有充分的发挥出多核的优势。

如果能采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。

所以,Redis 6.0采用多个IO线程来处理网络请求,网络请求的解析可以由其他线程完成,然后把解析后的请求交由主线程进行实际的内存读写。提升网络请求处理的并行度,进而提升整体性能。

但是,Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。

那么,在引入多线程之后,如何解决并发带来的线程安全问题呢?

Redis 6.0 只有在网络请求的接收和解析,以及请求后的数据通过网络返回时,使用了多线程。而数据读写操作还是由单线程来完成的,所以,这样就不会出现并发问题了。

etcd redis