Zer0e's Blog

2024面试复盘12

字数统计: 10.4k阅读时长: 37 min
2024/08/14 Share

bin log, redo log, undo log

MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。

redo log

redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。

比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。

MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool 中。

后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。

更新表数据的时候,也是如此,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。

然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里,接着刷盘到 redo log 文件里。

理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。

InnoDB 将 redo log 刷到磁盘上有几种情况:

  1. 事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过innodb_flush_log_at_trx_commit参数控制,后文会提到)。
  2. log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
  3. 事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘。
  4. Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。
  5. 后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。
  6. 正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去。

总之,InnoDB 在多种情况下会刷新重做日志,以保证数据的持久性和一致性。

我们要注意设置正确的刷盘策略innodb_flush_log_at_trx_commit 。根据 MySQL 配置的刷盘策略的不同,MySQL 宕机之后可能会存在轻微的数据丢失问题。

innodb_flush_log_at_trx_commit 的值有 3 种,也就是共有 3 种刷盘策略:

  • 0:设置为 0 的时候,表示每次事务提交时不进行刷盘操作。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。
  • 1:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。
  • 2:设置为 2 的时候,表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache(文件系统缓存)。page cache 是专门用来缓存文件的,这里被缓存的文件就是 redo log 文件。这种方式的性能和安全性都介于前两者中间。

刷盘策略innodb_flush_log_at_trx_commit 的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。

另外,InnoDB 存储引擎有一个后台线程,每隔1 秒,就会把 redo log buffer 中的内容写到文件系统缓存(page cache),然后调用 fsync 刷盘。

一个没有提交事务的 redo log 记录,也可能会刷盘。

因为在事务执行过程 redo log 记录是会写入redo log buffer 中,这些 redo log 记录会被后台线程刷盘。

日志文件组

硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo日志文件大小都是一样的。

比如可以配置为一组4个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录4G的内容。

在这个日志文件组中还有两个重要的属性,分别是 write pos、checkpoint

  • write pos 是当前记录的位置,一边写一边后移
  • checkpoint 是当前要擦除的位置,也是往后推移

每次刷盘 redo log 记录到日志文件组中,write pos 位置就会后移更新。

每次 MySQL 加载日志文件组恢复数据时,会清空加载过的 redo log 记录,并把 checkpoint 后移更新。

write poscheckpoint 之间的还空着的部分可以用来写入新的 redo log 记录。

如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。

在 MySQL 8.0.30 之前可以通过 innodb_log_files_in_groupinnodb_log_file_size 配置日志文件组的文件数和文件大小,但在 MySQL 8.0.30 及之后的版本中,这两个变量已被废弃,即使被指定也是用来计算 innodb_redo_log_capacity 的值。而日志文件组的文件数则固定为 32,文件大小则为 innodb_redo_log_capacity / 32

bin log

redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。

而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。

不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。

可以说 MySQL 数据库的数据备份、主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。

binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写。

binlog 日志有三种格式,可以通过binlog_format参数指定。

  • statement
  • row
  • mixed

指定statement,记录的内容是SQL语句原文,比如执行一条update T set update_time=now() where id=1,记录的内容如下。

同步数据时,会执行记录的SQL语句,但是有个问题,update_time=now()这里会获取当前系统时间,直接执行会导致与原库的数据不一致。

为了解决这种问题,我们需要指定为row,记录的内容不再是简单的SQL语句了,还包含操作的具体数据。

row格式记录的内容看不到详细信息,要通过mysqlbinlog工具解析出来。

update_time=now()变成了具体的时间update_time=1627112756247,条件后面的@1、@2、@3 都是该行数据第 1 个~3 个字段的原始值(假设这张表只有 3 个字段)。

这样就能保证同步数据的一致性,通常情况下都是指定为row,这样可以为数据库的恢复与同步带来更好的可靠性。

但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗 IO 资源,影响执行速度。

所以就有了一种折中的方案,指定为mixed,记录的内容是前两者的混合。

MySQL 会判断这条SQL语句是否可能引起数据不一致,如果是,就用row格式,否则就用statement格式。

写入机制

binlog 的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到 binlog 文件中。

因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache

我们可以通过binlog_cache_size参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap)。

writefsync的时机,可以由参数sync_binlog控制,默认是1

0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync

虽然性能得到提升,但是机器宕机,page cache里面的 binlog 会丢失。

为了安全起见,可以设置为1,表示每次提交事务都会执行fsync,就如同 redo log 日志刷盘流程 一样。

最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync

在出现 IO 瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。

同样的,如果机器宕机,会丢失最近N个事务的 binlog 日志。

两阶段提交

redo log(重做日志)让 InnoDB 存储引擎拥有了崩溃恢复能力。

binlog(归档日志)保证了 MySQL 集群架构的数据一致性。

虽然它们都属于持久化的保证,但是侧重点不同。

在执行更新语句过程,会记录 redo log 与 binlog 两块日志,以基本的事务为单位,redo log 在事务执行过程中可以不断写入,而 binlog 只有在提交事务时才写入,所以 redo log 与 binlog 的写入时机不一样。

回到正题,redo log 与 binlog 两份日志之间的逻辑不一致,会出现什么问题?

我们以update语句为例,假设id=2的记录,字段c值是0,把字段c值更新成1SQL语句为update T set c=1 where id=2

假设执行过程中写完 redo log 日志后,binlog 日志写期间发生了异常,会出现什么情况呢?

由于 binlog 没写完就异常,这时候 binlog 里面没有对应的修改记录。因此,之后用 binlog 日志恢复数据时,就会少这一次更新,恢复出来的这一行c值是0,而原库因为 redo log 日志恢复,这一行c值是1,最终数据不一致。

为了解决两份日志之间的逻辑一致问题,InnoDB 存储引擎使用两阶段提交方案。

原理很简单,将 redo log 的写入拆成了两个步骤preparecommit,这就是两阶段提交

使用两阶段提交后,写入 binlog 时发生异常也不会有影响,因为 MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于prepare阶段,并且没有对应 binlog 日志,就会回滚该事务。

undo log

每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。

undo log 属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。并且,undo-log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。

undo log 是采用 segment(段)的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment(undo 日志段),undo log segment 包含在 rollback segment(回滚段)中。事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment,这有助于管理多个并发事务的回滚需求。

通常情况下, rollback segment header(通常在回滚段的第一个页)负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分,通常在回滚段的第一个页。history list 是 rollback segment header 的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log。这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。

另外,MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_IDRead View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改。

总结

MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性

MySQL 数据库的数据备份、主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。

== 和 equals的区别

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

equals() 方法存在两种使用情况:

  • 类没有重写 equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Objectequals()方法。
  • 类重写了 equals()方法:一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

为什么重写 equals() 时必须重写 hashCode() 方法?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

索引是怎么使用?失效时机?

索引是一种用于加速数据库表中数据检索的结构。它类似于书籍中的目录,通过对数据的关键列建立索引,可以快速定位所需的数据行,而无需全表扫描。

类型

  • B-Tree 索引:最常见的索引类型,适用于大多数查询操作。
  • Hash 索引:基于哈希表,适用于精确匹配查询。
  • Full-Text 索引:用于全文搜索。
  • R-Tree 索引:用于空间数据类型(如 GIS 数据)的查询。

优点

  • 使用索引可以大大加快数据的检索速度(大大减少检索的数据量), 减少 IO 次数,这也是创建索引的最主要的原因。
  • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

缺点

  • 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
  • 索引需要使用物理文件存储,也会耗费一定空间。

但是,使用索引一定能提高查询性能吗?

大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。

索引失效场景

创建了组合索引,但查询条件未遵守最左匹配原则;

在索引列上进行计算、函数、类型转换等操作;

以 % 开头的 LIKE 查询比如 LIKE '%abc';;

查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;

IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);

发生隐式转换;

1
SELECT * FROM users WHERE username = 123;  -- 假设 username 是字符串类型,索引失效

查询条件使用 !=<>

使用 IS NULLIS NOT NULL

排序操作(ORDER BY)中混合使用 ASC 和 DESC

表中的数据量很少

mysql中的锁

乐观锁与悲观锁

乐观锁 基于乐观的假设,认为多个事务并发修改数据时不会发生冲突。它通常不直接使用数据库的锁机制,而是通过版本号或时间戳等机制来实现。

在数据库表中添加一个 version 字段,表示数据的版本。当事务读取数据时,也会读取这个 version。在提交更新时,事务会检查当前数据库中的 version 是否与自己读取时的一致。如果一致,才会更新数据,并将 version 加1;如果不一致,表示有其他事务已经修改了该数据,当前事务需要进行重试或处理冲突。

悲观锁 采取悲观的态度,假设并发修改数据时会发生冲突。因此,在操作数据之前,会先锁住数据,以防止其他事务对其进行修改。悲观锁通常通过数据库的锁机制来实现。

使用 SELECT ... FOR UPDATELOCK IN SHARE MODE 语句显式地对数据行进行加锁。在使用 FOR UPDATE 时,该行数据会被锁住,其他事务在同一行上执行 SELECT FOR UPDATE 或更新操作时会被阻塞,直到锁被释放。

在事务中执行更新、删除操作时,MySQL 会自动为相关数据行加锁。

表级锁和行级锁

表级锁: MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。

行级锁: MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。

InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。当我们执行 UPDATEDELETE 语句时,如果 WHERE条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁。这个在我们日常工作开发中经常会遇到,一定要多多注意!!

行锁有哪些

记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。

间隙锁(Gap Lock):锁定一个范围,不包括记录本身。

临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。

共享锁和排他锁

不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类:

  • 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
  • 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
S 锁 X 锁
S 锁 不冲突 冲突
X 锁 冲突 冲突

由于 MVCC 的存在,对于一般的 SELECT 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。

1
2
3
4
5
6
# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用
SELECT ... LOCK IN SHARE MODE;
# 共享锁 可以在 MySQL 8.0 中使用
SELECT ... FOR SHARE;
# 排他锁
SELECT ... FOR UPDATE;

意向锁

如果需要用到表锁的话,如何判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。

意向锁是表级锁,共有两种:

  • 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。

意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

意向锁之间是互相兼容的。

IS 锁 IX 锁
IS 锁 兼容 兼容
IX 锁 兼容 兼容

意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。

IS 锁 IX 锁
S 锁 兼容 互斥
X 锁 互斥 互斥

快照读和当前读

快照读(一致性非锁定读)就是单纯的 SELECT 语句,但不包括下面这两类 SELECT 语句。

1
2
3
4
5
SELECT ... FOR UPDATE
# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用
SELECT ... LOCK IN SHARE MODE;
# 共享锁 可以在 MySQL 8.0 中使用
SELECT ... FOR SHARE;

快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。

快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照。

只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读:

  • 在 RC 级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据。
  • 在 RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本。

快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。

当前读 (一致性锁定读)就是给行记录加 X 锁或 S 锁。

当前读的一些常见 SQL 语句类型如下:

1
2
3
4
5
6
7
8
9
10
# 对读的记录加一个X锁
SELECT...FOR UPDATE
# 对读的记录加一个S锁
SELECT...LOCK IN SHARE MODE
# 对读的记录加一个S锁
SELECT...FOR SHARE
# 对修改的记录加一个X锁
INSERT...
UPDATE...
DELETE...

自增锁

关系型数据库设计表的时候,通常会有一列作为自增主键。InnoDB 中的自增主键会涉及一种比较特殊的表级锁— 自增锁(AUTO-INC Locks)

更准确点来说,不仅仅是自增主键,AUTO_INCREMENT的列都会涉及到自增锁,毕竟非主键也可以设置自增长。

如果一个事务正在插入数据到有自增列的表时,会先获取自增锁,拿不到就可能会被阻塞住。这里的阻塞行为只是自增锁行为的其中一种,可以理解为自增锁就是一个接口,其具体的实现有多种。具体的配置项为 innodb_autoinc_lock_mode (MySQL 5.1.22 引入),可以选择的值如下

innodb_autoinc_lock_mode 介绍
0 传统模式
1 连续模式(MySQL 8.0 之前默认)
2 交错模式(MySQL 8.0 之后默认)

交错模式下,所有的“INSERT-LIKE”语句(所有的插入语句,包括:INSERTREPLACEINSERT…SELECTREPLACE…SELECTLOAD DATA等)都不使用表级锁,使用的是轻量级互斥锁实现,多条插入语句可以并发执行,速度更快,扩展性也更好。

不过,如果你的 MySQL 数据库有主从同步需求并且 Binlog 存储格式为 Statement 的话,不要将 InnoDB 自增锁模式设置为交叉模式,不然会有数据不一致性问题。这是因为并发情况下插入语句的执行顺序就无法得到保障。

zookeeper为什么可以实现强一致性的分布式锁

ZooKeeper 提供了强顺序一致性,这意味着客户端对同一个 ZooKeeper 节点的更新操作是按照顺序执行的,并且所有客户端都能看到相同的更新顺序。通过这种顺序一致性,ZooKeeper 可以确保所有客户端在同一时间看到的数据状态是一致的。

ZooKeeper 提供了临时顺序节点的功能。客户端在创建一个临时顺序节点时,会获得一个全局唯一且递增的序号,这个节点在客户端会话结束时(比如客户端崩溃或与 ZooKeeper 的连接断开)会自动删除。利用这一特性,可以实现分布式锁。

避免死锁与竞争

  • 自动删除节点:由于使用的是临时顺序节点,当客户端因故障与 ZooKeeper 断开连接时,ZooKeeper 会自动删除该客户端创建的节点,避免锁资源被永久占用,从而防止死锁的发生。
  • 竞争公平性:因为所有节点都是按顺序创建的,锁的获取是基于节点的顺序号,先创建的节点先获得锁,这保证了锁的公平性,不会因为客户端的网络延迟等原因导致锁的争夺不公平。

ZooKeeper 使用的是 Zab(ZooKeeper Atomic Broadcast)协议 作为其核心的分布式一致性算法。

Zab 协议在 ZooKeeper 中主要用于以下两个目的:

  • Leader 选举:在 ZooKeeper 集群中选出一个主节点(Leader),其余节点作为从节点(Followers)。所有写请求都通过 Leader 处理,再由 Leader 将更新广播给 Followers。
  • 原子广播(Atomic Broadcast):确保所有节点以相同的顺序应用相同的更新,保持数据的一致性。

Zab 协议的核心流程

  1. Leader 选举:当 ZooKeeper 集群启动或现任 Leader 出现故障时,Zab 协议会启动 Leader 选举过程。集群中的所有节点会根据各自的投票信息选出一个新的 Leader。选举过程中,节点会根据各自的 ZXID(事务 ID)来决定投票,ZXID 更大的节点具有更高的优先级。
  2. 同步数据:当新的 Leader 被选出后,Leader 会与 Followers 同步数据,确保所有节点的状态一致。这通常涉及将 Followers 的数据状态更新到最新的事务版本。
  3. 原子广播:在广播模式下,所有的写请求都被发送到 Leader 处理。Leader 将这些写请求封装成事务提案(Proposal),并将其以原子广播的方式发送给所有 Followers。所有 Followers 都必须对提案进行确认(Ack),一旦 Leader 收到了半数以上节点的确认,它就会将事务提交,并通知所有节点应用该事务。
  4. 保证一致性:Zab 协议通过严格的顺序更新和多节点确认机制,确保即使在节点崩溃或网络分区等情况下,ZooKeeper 集群仍然能够保证数据的一致性和可用性。

缓存击穿,穿透,雪崩

缓存击穿

缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

举个例子:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。

永不过期(不推荐):设置热点数据永不过期或者过期时间比较长。

提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。

加锁(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。

缓存穿透

缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

解决方法如下

缓存无效 key

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

布隆过滤器

布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

接口限流

根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。

后面提到的缓存击穿和雪崩都可以配合接口限流来解决,毕竟这些问题的关键都是有很多请求落到了数据库上造成数据库压力过大。

缓存穿透和缓存击穿有什么区别

缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。

缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)

缓存雪崩

缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。

有哪些解决办法

针对 Redis 服务不可用的情况:

  1. Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案,详细介绍可以参考:Redis 集群详解(付费)open in new window
  2. 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。

针对大量缓存同时失效的情况:

  1. 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
  2. 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  3. 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。

聚簇索引与非聚簇索引

聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。

在 MySQL 中,InnoDB 引擎的表的 .ibd文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。

优点

  • 查询速度非常快:聚簇索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
  • 对排序查找和范围查找优化:聚簇索引对于主键的排序查找和范围查找速度非常快。

缺点

  • 依赖于有序的数据:因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
  • 更新代价大:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。

非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。

非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。

优点

更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。

缺点

  • 依赖于有序的数据:跟聚簇索引一样,非聚簇索引也依赖于有序的数据
  • 可能会二次查询(回表):这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。

非聚簇索引不一定回表查询。

用户准备使用 SQL 查询用户名,而用户名字段正好建立了索引。

1
SELECT name FROM table WHERE name='guang19';

那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。

什么是回表

它描述了在使用非聚簇索引(或辅助索引)进行查询时,需要再通过主键索引(或聚簇索引)去获取完整的行数据的过程。

redis键过长会有什么影响?

懵了一下,故意刁难人吧,哪个正常人会把key弄得很长?

  1. 内存占用增加

  2. 网络带宽消耗增加

  3. 查询性能下降。在 Redis 中,键的比较是通过逐字节比较的方式进行的,因此键越长,比较操作的时间就越长。在高并发和大量键的情况下,这种额外的比较时间会影响查询性能。

  4. 持久化和数据加载时间增加

  5. 键空间效率低。Redis 使用的是字典数据结构(hash table)来存储键值对,过长的键可能导致 hash 冲突的概率增加,进而影响 Redis 的哈希性能。

redis中hash结构有哪些使用场景

  1. 存储用户信息。用户的基本信息(如用户名、邮箱、年龄等)可以通过哈希结构存储在一个键中:
1
2
3
4
5
user:1001 -> {
"name": "Alice",
"email": "alice@example.com",
"age": "30"
}
  1. 会话管理。例如存储session。

SpringCloud组件有哪些

  • Spring Cloud Config
  • Spring Cloud Netflix:Spring Cloud Netflix是对Netflix开发的一套分布式服务框架的封装,包括服务的发现和注册,负载均衡、断路器、REST客户端、请求路由等。它是Spring Cloud的一部分。包括 Eureka、Ribbon、Hystrix、Zuul、Gateway。
  • Spring Cloud Gateway
  • Spring Cloud OpenFeign
  • Spring Cloud Sleuth: 分布式追踪工具,用于在微服务架构中跟踪请求的流转路径,支持与 Zipkin、Jaeger 等分布式追踪系统集成。
  • Spring Cloud Bus:用于将消息总线连接到多个分布式系统的节点,通常与 Spring Cloud Config 结合使用,实现配置的动态刷新。

国内常用的SpringCloudAlibaba组件有:

  • Nacos: 提供服务发现、配置管理、动态 DNS 和服务健康检查的功能。Nacos 是 Spring Cloud Alibaba 中的核心组件,用于替代 Spring Cloud Netflix 的 Eureka 和 Spring Cloud Config。
  • Sentinel: 提供流量控制、熔断降级、系统负载保护等功能。Sentinel 是一个高可用保护的流量管理工具,能够对微服务进行全面的保护。
  • RocketMQ: 分布式消息中间件,支持高吞吐量、低延迟和高可用的消息传递。Spring Cloud Alibaba 提供了对 RocketMQ 的集成,方便微服务之间的消息通信。
  • Dubbo: 高性能的 RPC 框架,支持微服务间的远程调用。Dubbo 提供了服务治理、服务监控等功能,并与 Spring Cloud 集成,实现微服务的无缝对接。
  • Seata: 分布式事务管理框架,支持微服务架构中的分布式事务,能够在多服务、多数据库的场景下保证事务的最终一致性。

在阿里云上商用的组件还有:

  • Alibaba Cloud OSS: 阿里云的对象存储服务,提供大规模、高可用的云存储解决方案。Spring Cloud Alibaba 提供了与 OSS 的集成,使开发者能够方便地在微服务中使用对象存储。
  • Alibaba Cloud SMS: 阿里云的短信服务,支持通过 API 发送短信验证码、通知等。Spring Cloud Alibaba 集成了短信服务,方便在微服务中使用短信功能。
  • Alibaba Cloud ACM (Application Configuration Management): 应用配置管理服务,类似于 Nacos 的配置管理功能,但更加侧重于企业级应用的配置管理。Spring Cloud Alibaba 集成了 ACM,提供了配置管理的高级功能。
  • Alibaba Cloud SchedulerX: 分布式任务调度平台,支持高可用、高并发的任务调度。SchedulerX 可以用于定时任务、分布式任务和调度任务的管理。
  • Alibaba Cloud MNS (Message Notification Service): 消息通知服务,支持大规模的异步消息通知。Spring Cloud Alibaba 提供了对 MNS 的集成,使开发者能够轻松实现消息通知功能。
  • Alibaba Cloud ARMS (Application Real-Time Monitoring Service): 应用实时监控服务,提供了对分布式系统的全方位监控,包括性能监控、日志分析、报警等。Spring Cloud Alibaba 集成了 ARMS,支持对微服务的实时监控。
CATALOG
  1. 1. bin log, redo log, undo log
    1. 1.1. redo log
      1. 1.1.1. 一个没有提交事务的 redo log 记录,也可能会刷盘。
      2. 1.1.2. 日志文件组
    2. 1.2. bin log
      1. 1.2.1. 写入机制
      2. 1.2.2. 两阶段提交
    3. 1.3. undo log
    4. 1.4. 总结
  2. 2. == 和 equals的区别
  3. 3. 为什么重写 equals() 时必须重写 hashCode() 方法?
  4. 4. 索引是怎么使用?失效时机?
    1. 4.1. 索引失效场景
  5. 5. mysql中的锁
    1. 5.1. 乐观锁与悲观锁
    2. 5.2. 表级锁和行级锁
    3. 5.3. 行锁有哪些
    4. 5.4. 共享锁和排他锁
    5. 5.5. 意向锁
    6. 5.6. 快照读和当前读
    7. 5.7. 自增锁
  6. 6. zookeeper为什么可以实现强一致性的分布式锁
    1. 6.1. Zab 协议的核心流程
  7. 7. 缓存击穿,穿透,雪崩
    1. 7.1. 缓存击穿
    2. 7.2. 缓存穿透
    3. 7.3. 缓存穿透和缓存击穿有什么区别
    4. 7.4. 缓存雪崩
  8. 8. 聚簇索引与非聚簇索引
  9. 9. 什么是回表
  10. 10. redis键过长会有什么影响?
  11. 11. redis中hash结构有哪些使用场景
  12. 12. SpringCloud组件有哪些