目录
MVCC
解决什么问题?
实现原理
隐式字段
undo log
Read View(读视图)
InnoDB 对 MVCC 的实现
锁
分类
锁升级?
InnoDB 的行锁?
死锁避免?
乐观锁和悲观锁
日志
主从复制原理
主从复制的作用
MySQL主从复制解决的问题
涉及3个线程
复制过程
读写分离
MVCC
一种多版本并发控制机制,用处就是在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。
解决什么问题?
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。同时还可以解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题。
实现原理
它的实现原理主要是依赖记录中的 3 个隐式字段、undo log、Read View 来实现的。
通过保存数据在某个时间点的快照来实现的.不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
-
DB_ROW_ID 6byte, 隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
-
DB_TRX_ID 6byte, 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
-
DB_ROLL_PTR 7byte, 回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
-
DELETED_BIT 1byte, 记录被更新或删除并不代表真的删除,而是删除flag变了
undo log
InnoDB把这些为了回滚而记录的这些东西称之为undo log。
注意,由于查询操作并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log。
undo log主要分为3种:
-
Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
-
Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
-
Delete undo log
:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
-
删除操作都只是设置一下老记录的DELETED_BIT,并不真正将过时的记录删除。
-
为了节省磁盘空间,InnoDB有专门的purge线程来清理DELETED_BIT为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的DELETED_BIT为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
-
Read View(读视图)
定义:说白了Read View就是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
InnoDB 对 MVCC 的实现
MVCC
的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB
通过数据行的 DB_TRX_ID
和 Read View
来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR
找到 undo log
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View
之前已经提交的修改和该事务本身做的修改
锁
分类
表级锁: MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
行级锁: MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。
表锁和行锁都有共享锁和排它锁
-
共享锁:又称读锁,多个用户可以同一时刻读取同一个资源,相互之间没有影响。事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。不堵塞,
-
排它锁:又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
意向锁是表级锁,共有两种:
意向共享锁:事务有意向对表中的某些记录加共享锁,加共享锁前必须先取得该表的意向共享锁。
意向排他锁:事务有意向对表中的某些记录加排他锁,加排他锁之前必须先取得该表的意向排他锁。
锁升级?
-
MySQL 行锁只能加在索引上,如果操作不走索引,就会升级为表锁。因为 InnoDB 的行锁是加在索引上的,如果不走索引,自然就没法使用行锁了,原因是 InnoDB 是将主键索引 和相关的行数据共同放在 B+ 树的叶节点。InnoDB 一定会有一个 主键,二级索引 查找的时候,也是通过找到对应的主键,再找对应的数据行。
-
当非唯一索引上记录数超过一定数量时,行锁也会升级为表锁。测试发现当非唯一索引相同的内容不少于整个表记录的二分之一时会升级为表锁。因为当非唯一索引相同的内容达到整个记录的二分之一时,索引需要的性能比全文检索还要大,查询语句优化时会选择不走索引,造成索引失效,行锁自然就会升级为表锁。
InnoDB 的行锁?
InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式:
-
记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。
-
间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
-
临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。
死锁避免?
定义:死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
-
设置获取锁的超时时间,至少能保证最差情况下,可以退出程序,不至于一直等待导致死锁;
-
设置按照同一顺序访问资源,类似于串行执行;
-
避免事务中的用户交叉;
-
保持事务简短并在一个批处理中;
-
使用低隔离级别;
乐观锁和悲观锁
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:一般会使用版本号机制或CAS算法实现。用在读多的场景。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制。用在写多的场景。
日志
更新语句的流程会涉及到三种日志:
-
undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。
-
redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
-
binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;
写入原理:
主从复制原理
-
主从复制:将主数据库中的DDL和DML操作通过二进制日志(BINLOG)传输到从数据库上,然后将这些日志重新执行(重做);从而使得从数据库的数据与主数据库保持一致。
-
原理
-
在主库上把数据更高记录到二进制日志
-
从库将主库的日志复制到自己的中继日志
-
从库读取中继日志的事件,将其重放到从库数据中
-
主从复制的作用
-
主数据库出现问题,可以切换到从数据库。
-
可以进行数据库层面的读写分离。
-
可以在从数据库上进行日常备份。
MySQL主从复制解决的问题
-
数据分布:随意开始或停止复制,并在不同地理位置分布数据备份
-
负载均衡:降低单个服务器的压力
-
高可用和故障切换:帮助应用程序避免单点失败
-
升级测试:可以用更高版本的MySQL作为从库
涉及3个线程
-
binlog 线程 : 负责将主服务器上的数据更改写入二进制日志中。
I/O 线程 : 负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。
SQL 线程 : 负责读取中继日志并重放其中的 SQL 语句。
复制过程
-
主节点在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。
-
从节点开启一个I/O Thread,该线程在主节点打开一个普通连接,主要工作是二进制日志传输。如果读取的进度已经跟上了主节点,就进入睡眠状态并等待主节点产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。
-
SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。
读写分离
主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。
读写分离能提高性能的原因在于:
-
主从服务器负责各自的读和写,极大程度缓解了锁的争用;
-
从服务器可以使用 MyISAM,提升查询性能以及节约系统开销;
-
增加冗余,提高可用性。