MySql 事务原理 - 面试
目录
幻读:一个事务内两次读取同一个范围内的记录得到的结果集不一样,当前读和快照读不一致
一、事务
事务是什么:事务的本质是并发控制的单元,是用户定义的一个操作序列。这些操作要么都做,要么都不做,是 一个不可分割的工作单位(原子操作)。
什么时候使用事务:并发连接访问的时候。
事务的目的:事务将数据库从一种一致性状态转换为另一种一致性状态;保证系统始终处于一个完整且正确的状态。
-- 显示开启事务
START TRANSACTION | BEGIN
-- 提交事务,并使得已对数据库做的所有修改持久化
COMMIT
-- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改
ROLLBACK
-- 创建一个保存点,一个事务可以有多个保存点
SAVEPOINT identifier
-- 删除一个保存点
RELEASE SAVEPOINT identifier
-- 事务回滚到保存点
ROLLBACK TO [SAVEPOINT] identifier
我们写入的命令会进入BufferPool中,Buffer pool 缓存表和索引数据,采用 LRU 算法让 Buffer pool 只缓存比较热的数据 , buffer pool 中的数据修改没有刷到磁盘, 怎么确保内存中数据安全(mysql 关闭时,内存数据丢失)?
Change buffer 缓存辅助(二级)索引的数据变更(DML 操作)这些数据并不在 buffer pool 中, Change buffer 中的数据将会异步 merge 到 buffer pool 中,当下次从磁盘当中读取非唯一索引的 数据;同时会定期合并到索引页中。 free list 组织 buffer pool 中未使用的缓存页;flush list 组织 buffer pool 中脏页,也就是待刷盘的 页;lru list 组织 buffer pool 中冷热数据,当 buffer pool 没有空闲页,将从 lru list 中最久未使用的 数据进行淘汰
ACID特性:
原子性:事务操作要么都做(提交),要么都不做(回滚);事务是访问并更新数据库各种数据项的一个程序执行单元,是不可分割的工作单位;通过 undolog 来实现回滚操作。undolog 记录的是事务每步具体操作,当回滚时,回放事务具体操作的逆运算。
一致性:事务的前后,所有的数据都保持一个一致的状态,不能违反数据的一致性检测(完整性约束检查);其中这个完整性约束是数据库做的保证,比如唯一约束这种。
隔离性:主要规定多个事务访问同一数据资源,各个事务对该数据资源访问的行为,不同的隔离性是应对不同的现象(脏读、不可重复读、幻读);防止多个并发事务交叉执行导致数据不一致。
持久性:事务一旦完成,要将数据所做的变更记录下来,包括数据存储和多副本的网络备份;使用 redolog 记录物理日志(写入磁盘)。
undolog:通过MVCC记录事务DML操作提交后产生的行数据版本信息。记录DML操作步骤,用于回滚业务,通过逆运算回滚。
redolog:事务提交后,记录DML操作对应物理页修改的内容。
二、隔离级别
我们在上述隔离性中提到了不同的隔离性应对不同的现象,首先我们的隔离级别都包括:read_uncommitted(读未提交)、read_committed(读已提交)、repeatable_read(可重复读)、serializable(可串行化)。从左往右隔离级别越来越高,效率越来越低。(MVCC操作后面会讲)
read_uncommitted(读未提交):读(不做任何处理),写(自动加X锁)。脏读、不可重复读、幻读。
read_committed(读已提交)(RC):读(通过MVCC,读取最新版本的数据),写(自动加X锁)。不可重复读、幻读。
repeatable_read(可重复读)(RR):读(通过MVCC,读取开启事务前的行数据),写(自动加X锁)。幻读。
serializable(可串行化):读(自动加S锁),写(自动加X锁)。
三、不同隔离级别并发异常
我们先创建一个表和一些数据:编号1,2,3,7的都为1000块钱。
DROP TABLE IF EXISTS `account_t`;
CREATE TABLE `account_t` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) DEFAULT NULL,
`money` INT(11) DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
)ENGINE = INNODB AUTO_INCREMENT=0 DEFAULT CHARSET = utf8;
INSERT INTO `account_t` VALUES (1,'A',1000), (2, 'B', 1000),(3, 'C', 1000),(7, 'M', 1000);
脏读:一个事务读到另一个未提交事务修改的数据
两个事务同时开启,为事务一和事务二,事务二先查看A的钱:1000,事务一修改A的钱为1100,此时一并未提交事务,事务二中再次查看A的钱,也变为了1100。一个事务读到另一个未提交事务修改的数据。两个都在事务内发生。
-- 设置隔离级别为读未提交
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 开始事务1
BEGIN
-- 开启事务2 (这里开启事务2是在开启另一个窗口进行的)
BEGIN
-- 脏读事务2
SELECT money FROM account_t WHERE name = 'A'; -- 返回1000
-- 修改事务1
UPDATE account_t SET money = money + 100 WHERE name = 'A';
-- 脏读事务2
SELECT money FROM account_t WHERE name = 'A'; --返回1100
-- 提交事务1
COMMIT;
-- 提交事务2
COMMIT
不可重复读:一个事务内两次读取同一个数据不一样
首先我们先把数据恢复如初,仍然是两个事务,其中事务二先查看A的钱:1000,然后事务一进行修改为1100并且提交,事务二此时再次查看A的钱:1100,这里是读到了不同的数据了。
-- 设置事务为读已提交
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 开启事务1
BEGIN
-- 开启事务2
BEGIN
-- 事务2
SELECT money FROM account_t WHERE name = 'A'; --1000
-- 事务1
UPDATE account_t SET money = money + 100 WHERE name = 'A';
COMMIT;
-- 事务2
SELECT money FROM account_t WHERE name = 'A'; --1100
COMMIT
幻读:一个事务内两次读取同一个范围内的记录得到的结果集不一样,当前读和快照读不一致
首先我们先把数据恢复如初,仍然是两个事务,事务二先查看id大于3的只有7,事务一插入4号,此时在事务二中并未发现4号,现在我们进行插入4号元素,会发现报错。
-- 设置REPEATABLE READ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 开启事务1
BEGIN
-- 开启事务2
BEGIN
-- 幻读事务2
SELECT * FROM account_t WHERE id > 3 ; --7
-- 幻读事务1
INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); -插入四号
COMMIT
-- 幻读事务2
INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); -插入四号
-- 报错,不能插入相同id的值
我们MySql数据库默认采用的隔离级别为:REPEATABLE READ 也就是可以发生幻读,如果我们并不想提升隔离级别来解决这种情况的话,我们可以手动加锁。这个加锁后面会讲。
-- 解决幻读问题
-- 设置REPEATABLE READ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 开启事务1
BEGIN
-- 开启事务2
BEGIN
-- 事务2
SELECT * FROM account_t WHERE id > 3 for update ; --通过for update进行加锁,3之后的行数据全部加锁
-- 事务1
INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); --事务一中进行插入操作,但是由于3之后的数据加锁,所以这里被阻塞等待
-- 事务2
INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); -插入四号,可以正常插入
COMMIT --提交之后事务一报错。
四、MVCC
MVCC是多版本并发控制,保证数据的一致性和并发性。可以使多个事务在同时访问数据的时候,各自看到不同版本的数据,不会互相打扰,这样就可以避免锁和等待了。
read_view:
是一个事务开始时创建的数据视图,他决定了事务能够看到的数据库版本中的数据版本。其中包括:
m_ids:创建read_view时,已启动但未提交的事务id列表。
min_trx_id:创建read_view时,已启动但未提交的最小事务id。
max_trx_id:创建read_view时,预分配给下一个未开启事务的id。
creator_trx_id:创建read_view的事务id。
聚集索引记录的隐藏列:
我之前的文章讲过索引存储在表空间中:段、区、页、行,这里就存储在行中。
trx_id:事务修改记录时,trx_id记录该修改事务id。DML操作是要加锁的,不管是什么隔离级别。
roll_pointer:事务修改记录时,将旧记录写入undolog,该指针指向旧版本记录。
事务的可见性问题:
事务可以看到事务本身的修改。那么事务之间的可见性问题:
trx_id < min_trx_id :已提交的事务是可见的。
trx_id >= max_trx_id :后启动的事务是不可见的。
min_trx_id <= trx_id < max_trx_id:当trx_id 在id事务列表中,那就是已启动但未提交的不可见,当trx_id不在id事务列表中,那就是已提交可见的。
RC和RR的区别:
我们再讲隔离性的时候,RC和RR隔离级别都使用了MVCC。他俩有不同点。其中RC(读已提交):每次读取数据的时候,就会生成read_view。而RR(可重复读)启动事务时,才会生成新的read_view,一直使用直到事务提交。
所以我们RC隔离级别中由于每次读取就会生成新的read_view,就会看到最新的数据,因此我们会产生不可重复读。而RR中只有启动事务才会创建read_view所以解决了不可重复读。
那什么是快照读和当前读呢?
快照读:就是从之前拍摄的一个快照中来读取数据。当前读:就是读取当下最新的数据。快照读并未采用锁,而当前读使用了锁。
--快照读
select * from table where ?
--当前读
select * from table where ? lock in share mode
select * from table where ? for update
insert into table values()
update table set ? where ?
delete from table where ?
五、锁
我们先看看整个数据库是什么样的:某个数据库下有多个表,表下面有多个页,一个页下有多个记录。因此我们含有全局锁、表级锁、行级锁三种。
全局锁:
通过flush_tables_with_read_lock 使整个数据库处于只读状态。使用 unlock_tables,我们使用这个锁是为了使用全库备份。
表级锁:
表锁:lock_tables_'table'[read/write] 通过unlock_tables解锁。
元数据锁:curd和alter。
意向锁:快速判断表里是否记录加锁。
auto_inc锁:特殊表锁,实现自增约束,语句结束后释放锁(而非在事务结束时释放)。
行级锁:
记录锁(record_lock):S锁:共享锁,X锁:排他锁。
间隙锁(gap_lock):在RR,RC中使用。防止其他事务在记录间隙插入新的记录,从而避免幻读现象。(3,7)(7,正无穷)。
临键锁(next_key_lock):RR中使用。将记录锁和间隙锁连在一起就是临键锁。[3,7)(7,正无穷)。
我们看看在哪些地方使用什么锁:查询、删除、更新、插入。
查询:我们的MVCC在undolog中实现历史版本记录。S锁:lock_in_share_mode。X锁:for_update。不做任何处理:read_uncommotted使用的策略。
删除、更新:会自动添加X锁。
插入:insert_intention_lock(特殊的gap锁)意向锁:同时会使用X锁。auto_inc_lock:特殊表锁实现。
我们对锁的对象进行一下分类讨论:聚集索引、辅助唯一索引、辅助非唯一索引、无索引、范围查询。
六、死锁
死锁的原因:并发事务在执行过程中,因争夺资源而造成的一种互相等待的现象。我之前的文章讲解过。
相反加锁顺序死锁:不同表加锁顺序相反,相同表不同行加锁顺序相反。这些都会造成死锁的发生,我们只需要调整加锁顺序。
锁冲突造成死锁:RR隔离级别下,插入意向锁与gap锁冲突死锁。我们只需要降低隔离级别到RC。
本篇文章讲解完毕! 0voice · GitHub
原文地址:https://blog.csdn.net/2301_76446998/article/details/142360064
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!