什么是事务

一组操作,要么全部执行,要么全部不执行,保证数据最终一致性。

ACID特性

原子性(Atomicity):事务操作要么全部执行成功,要么全部执行失败。由undo Log 日志保证。(执行一半怎么办?)
一致性(Consistency): 事务最终目标,由AID特性与正确业务逻辑代码保证。 (业务规则被破坏?)
隔离性(Isolation): 事务并发执行时,保证事务之间数据隔离。由锁与mvcc机制保证。 (并发冲突怎么办?)
持久性(Durability): 事务提交后数据永久保存。由redo Log+ 双写缓冲区保证。 (崩溃后数据还在吗?)

隔离级别与并发问题

并发问题
一个事务修改了另一个未提交事务已经修改过的数据

脏写: 当前事务更新的数据覆盖了其他事务的更新的数据
-场景:事务A读取到数据a=1,由程序进行a+1逻辑计算后直接更新此行数据。假如事务A由程序进行逻辑计算时,表中数据a=1被其他事务更新,此时事务A依旧使用旧数据进行逻辑计算,并更新此行数据。
-解决:1:逻辑操作使用SQL在同一事务中一起处理。
2:乐观锁,更新数据时使用版本号,判断数据是否被修改过。

脏读: 事务A读到事务B已经修改但未提交的数据,如果事务B回滚,事务A读到数据无效,不符合数据一致性。(MVCC解决)

幻读: 事务A读取到事务B提交的新增数据。
-场景:事务B新增的数据,满足事务A范围查询条件,导致事务A两次查询范围结果不一致。(事务B删除满足事务A范围查询条件数据)。
-解决:间隙锁

不可重复读: 事务A同一条件查询语句,在不同时刻读取的同一行数据结果不同。(MVCC解决)

隔离级别
读未提交(read uncommit ): 可能会出现脏读,不可重复读,幻读问题
读已提交(read commit ): 解决脏读问题,可能会出现不可重复读,幻读问题。(读取数据时使用语句级快照)
核心机制是 “读取已提交的数据版本”,实现方式 MVCC(多版本并发控制) + Read View(读视图)
可重复读(repeatable read ): 解决脏读、不可重复读问题。幻读问题引入临建锁解决。(读取数据时使用事务级快照)
串行化(serializable ): 所有事务串行化执行。执行更新或查询数据时,其他事务都无法更新。(同一条数据读写都会加锁,x/s锁。 范围查询时范围内的所有行包括每行记录所在的间隙区间范围都会被加锁解决幻读。

语句级快照: 仅当前SELECT语句。(事务中每条SELECT语句执行时查询到的数据结果,rc)
事务级快照: 整个事务使用同一个快照。(事务中第一条SELECT语句查询到的数据结果,rr)

快照读: 读取历史数据
当前读: 读取当前版本数据
事务中查询数据使用快照读,修改数据时(update、delete)或加锁查询使用当前读(当前版本)

锁 :保证数据并发访问一致性与有效性

从性能上区分**

乐观锁(用版本对比来实现)
悲观锁:读锁,写锁

从对数据库操作的类型区分**

读锁(共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响。select * from T where id=1 lock in share mode

写锁(排它锁,X锁(exclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁。select * from T where id=1 for update

意向锁(Intention Lock,I锁):当有事务对一行数据加了x/s锁,同时给表加一个标识(意向锁)。当其他事务要加表锁时,不必逐行判断是否存在行锁与表锁冲突,直接读取是否存在这个标识。当表中数据量多时,直接通过判断是否存在意向锁提升加锁效率。

意向排他锁(IX锁):加s锁之前,需要先获取到意向共享锁

意向共享锁(IX锁):加x锁之前,需要先获取到意向排他锁

从对数据操作的粒度区分

表锁: 锁整张表,加锁快,开销小,无死锁,锁冲突最高,并发度最低。使用场景:数据迁移。
命令:
手动增加表锁:lock table 表名称 read(write),表名称2 read(write);
查看表上加过的锁:show open tables;
删除表锁:unlock tables;
注意点:读锁不堵塞其他进程读请求,但会堵塞写请求,写锁堵塞其他进程读写操作。(同一张表读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞)

行锁: 锁一行数据,加锁慢,开销大,有死锁,锁冲突概率低,并发度最高(InnoDB支持,myisam不支持)。行级锁允许大量并发读写操作,只在操作同一行时才互相等待

页锁: 锁一页数据,开销介于表锁和行锁之间,有死锁。锁定粒度介于表锁和行锁之间,并发度一般。只有BDB引擎支持页锁。

间隙锁(Gap Lock) :锁两个值之间空隙,只有在RR级别生效,解决幻读问题。

临键锁(Next-key Locks) :行锁与间隙锁的组合。

InnoDB与MYISAM的不同点

最大不同有两点:InnoDB支持事务(TRANSACTION)、支持行级锁。MYISAM不支持。

不同点

InnoDB默认行级锁 ,表锁用于意向锁或手动加表锁场景,select时是快照读不加锁,update/insert/delete才加行锁,写操作时操作同一行数据才会等待,读操作不堵塞任何读写操作(mvcc)。

myisam默认表锁,所有DML操作都会加表锁,读前加读锁,写前加写锁,写操作会堵塞所有其他读取表或修改表的操作,读操作不堵塞其他读操作,堵塞所有写操作。

InnoDB普通查询为快照读,使用mvcc机制,根本不需要加锁。更新操作自动添加排他锁,只需要锁住修改的行。但是如果更新操作没有索引时,无法确定锁住那些行,会使用表锁。InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。

MyISAM读操作会自动加上表级读锁。多个读操作可以同时进行,不会互相阻塞。但一旦有写请求,就必须等所有读锁都释放了才能执行。写操作 会自动加上表级写锁。会阻塞其他所有读写操作。对MyISAM表来说“写写互斥”和“读写互斥”是绝对的

锁等待如何分析

检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况
		命令:show status like 'innodb_row_lock%';
		Innodb_row_lock_time_avg  -> 每次等待所花平均时间
		Innodb_row_lock_waits -> 系统启动后到现在 总共等待的次数
		Innodb_row_lock_time -> 从系统启动到现在锁定  总时间长度
		
		Innodb_row_lock_time_max:从系统启动到现在 等待最长的一次所花时间
		Innodb_row_lock_current_waits: 当前正在等待锁定的数量

查看INFORMATION_SCHEMA系统库锁相关数据表:

‐‐ 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX;

‐‐ 查看锁,8.0之后需要换成这张表performance_schema.data_locks
select * from INFORMATION_SCHEMA.INNODB_LOCKS;

‐‐ 查看锁等待,8.0之后需要换成这张表performance_schema.data_lock_waits
select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

‐‐ 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
kill trx_mysql_thread_id;

‐‐ 查看锁等待详细信息
show engine innodb status;

--查看近期死锁日志信息:
show engine innodb status;

锁优化方式

	尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
	合理设计索引,尽量缩小锁的范围
	尽可能减少检索条件范围,避免间隙锁
	尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
	尽可能用低的事务隔离级别

MVCC多版本并发控制机制

undo日志版本链:每条记录在每次修改后,都会生成一个旧版本(Undo 日志),多个版本通过指针串联成链表.

Undo 日志的结构
trx_id :产生该版本的事务ID
roll_pointer: 指向上一个版本的指针
实际数据: 该版本的字段值

Read View(一致性读视图):事务执行快照读时,创建的一个"数据可见性判断依据",记录了当前系统中哪些事务是活跃的(未提交)

组成:
执行查询时 所有“未提交事务id数组”(数组里最小的id为min_id)
执行查询时 “已创建的最大事务id”(max_id)

大致结构:[min_id,trx_id1,trx_id2,…,trx_idn ],max_id

对比规则:
沿着 Undo 版本链查找可见版本
1、如果row的 trx_id 比 min_id 小,则是这一行是已提交的数据,是可见的。trx_id < min_id
2、如果row的 trx_id 比 max_id大,则是这一行是未来启动的事务,是不可见的。trx_id > max_id。若 row 的 trx_id 就是当前自己的事务,可见。
3、如果row的 trx_id 大于等于min_id 并且 小于等于max_id (min_id <=trx_id<= max_id)
3.1、若row的 trx_id在 “未提交事务id数组” 中,则是这一行是未提交的事务生成的,不可见。若 row 的 trx_id 就是当前自己的事务,可见。
3.2、若当前记录的 trx_id 不在 “未提交事务id数组” 中,则这个事务已经提交,可见。

例如:

-- -- 初始数据(假设最后提交事务 trx_id = 50) 版本记录1: id=1, name='张三', age=20  trx_id=50, roll_pointer=NULL
INSERT INTO users VALUES (1, '张三', 20);
COMMIT;

-- 事务A(trx_id=100)
BEGIN;
UPDATE users SET name='李四' WHERE id=1;  -- 版本记录2: id=1, name='李四', age=20 trx_id=100, roll_pointer ->版本记录1
-- 未提交

-- 事务B(trx_id=101)
BEGIN;
UPDATE users SET age=25 WHERE id=1;   -- 版本记录3: id=1, name='李四', age=25  trx_id=101, roll_pointer -> 版本记录 2
-- 未提交

-- 事务C(trx_id=102,当前查询事务)
BEGIN;
-- 将在这里执行 SELECT 查询 ,查询结果  id=1, name='张三', age=20
SELECT * FROM users WHERE id=1; 

执行流程:
步骤1:事务C 创建 Read View : [100, 101] 102 (问题:执行查询时,已创建的最大事务id 是 未提交状态,会不会放入未提交事务id数组中)
步骤2: 沿着 Undo 版本链从新到旧依次判断

	当前记录:版本记录3(id=1, name='李四', age=25  trx_id=101, roll_pointer -> 版本记录 2)
   ↓ 判断:trx_id=101 , 101 在  “未提交事务id数组” 中  → 不可见

	查看上一个版本:版本记录2( id=1, name='李四', age=20 trx_id=100, roll_pointer ->版本记录1)
   ↓ 判断: trx_id=100 ,101 在  “未提交事务id数组” 中  → 不可见

	查看上一个版本:版本记录1(  id=1, name='张三', age=20  trx_id=50, roll_pointer=NULL)
   ↓ 判断: trx_id=50 ,50 < min_id  →  可见

Read View 就是记录执行sql查询时当前时刻的数据库未提交事务与提交事务状态。通过read-view机制与undo版本链对比,不同的事务在版本链中读取不同版本数据。

RR级别:事务中每一次执行查询都是使用第一次查询当前时刻生成的read View视图,与当前的undolog版本链比对。每次查询都会根据第一次查询时数据库所有事务状态判断是否可见。实现可重复读。

RC级别:事务中每一次执行查询都会重新生成当前时刻的read View试图,与当前的undolog版本链比对。每次查询都会根据当时的数据库所有事务状态判断是否可见。每次查询最新数据。

查询操作方法需要使用事务吗?

	使用rc与rr进行查询数据时不同效果回答。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐