分布式事物处理_认识本地事物
什么是事物
事务就是针对数据库的一组操作,它可以由一条或多条SQL语句组 成,同一个事务的操作具备同步的特点,事务中的语句要么都执 行,要么都不执行。
举个栗子:
你去小卖铺买东西,一手交钱,一手交货就是一个事务的例子,交钱和交货必须全部成功,事务才算成功,任一个活动失败,事务将撤销所有已成功的活动。
什么是本地事物
在计算机系统中,更多的是通过关系型数据库来控制事务,这是利 用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用 主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。
数据库事务的四大特性ACID
总结
数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个 不可分割的执行单元,该执行单元中的所有操作要么都成功,要么 都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。
关系型数据库事务基础_并发事务带来的问题
并发事务带来的问题
数据库一般会并发执行多个事务,而多个事务可能会并发地对相同 的数据进行增加、删除、修改和查询操作,进而导致并发事务问 题。
脏写
当两个或两个以上的事务选择数据库中的同一行数据,并基于最初 选定的值更新该行数据时,因为每个事务之间都无法感知彼此的存 在,所以会出现最后的更新操作覆盖之前由其他事务完成的更新操 作的情况。也就是说,对于同一行数据,一个事务对该行数据的更新操作覆盖了其他事务对该行数据的更新操作。
解决方案: 让每个事物按照顺序串行的方式执行,按照一定的顺序一次进行写操作。
脏读
一个事务正在对数据库中的一条记录进行修改操作,在这个事务完 成并提交之前,当有另一个事务来读取正在修改的这条数据记录 时,如果没有对这两个事务进行控制,则第二个事务就会读取到没 有被提交的脏数据,并根据这些脏数据做进一步的处理,此时就会 产生未提交的数据依赖关系。我们通常把这种现象称为脏读,也就是一个事务读取了另一个事务未提交的数据。
解决方案: 先写后读,也就是写完之后再读。
不可重复读
一个事务读取了某些数据,在一段时间后,这个事务再次读取之前 读过的数据,此时发现读取的数据发生了变化,或者其中的某些记录已经被删除,这种现象就叫作不可重复读。
解决方案: 先读后写,也就是读完之后再写。
幻读
一个事务按照相同的查询条件重新读取之前读过的数据,此时发现 其他事务插入了满足当前事务查询条件的新数据,这种现象叫作幻读。
解决方案: 先读后写,也就是读完之后再写。
关系型数据库事务基础_MySQL事务隔离级别
MySQL中的InnoDB储存引擎提供SQL标准所描述的4种事务隔离级 别,分别为
读未提交 (Read Uncommitted)
读已提交 (ReadCommitted)
可重复读(Repeatable Read)
串行化 (Serializable)。
1、读未提交(Read Uncommitted):事务可以读取未提交的数据,也称作脏读(Dirty Read)。一 般很少使用。
2、读已提交(Read Committed):是大都是 DBMS (如:Oracle, SQLServer)默认事务隔离。执行两次同意的查询却有不同的结果,也叫不可重复读。
3、可重复读(Repeable Read):是 MySQL 默认事务隔离级别。能确保同一事务多次读取同一数据 的结果是一致的。可以解决脏读的问题,但理论上无法解决幻读(Phantom Read)的问题。
4、可串行化(Serializable):是最高的隔离级别。强制事务串行执行,会在读取的每一行数据上加锁,这样虽然能避免幻读的问题,但也可能导致大量的超时和锁争用的问题。很少会应用到这种级别,只有在非常需要确保数据的一致性且可以接受没有并发的应用场景下才会考虑。
MySQL事务隔离级别_模拟异常发生之脏读
前置知识
# 查看 MySQL 版本 select version(); # 开启事务 start transaction; # 提交事务 commit; # 回滚事务 rollback;
查看连接的客户端详情
每个 MySQL 命令行窗口就是一个 MySQL 客户端,每个客户端都可 以单独设置(不同的)事务隔离级别,这也是演示 MySQL 并发事务的基础。
show processlist;
新建数据库和测试数据
-- 创建数据库 drop database if exists testdb; create database testdb; use testdb; -- 创建表 create table userinfo( id int primary key auto_increment, name varchar(250) not null, balance decimal(10,2) not null default 0 ); -- 插入测试数据 insert into userinfo(id,name,balance) values(1,'Java',100),(2,'MySQL',200);
查询事务的隔离级别
select @@global.transaction_isolation,@@transaction_is olation;
设置客户端的事务隔离级别
通过以下 SQL 可以设置当前客户端的事务隔离级别:
set session transaction isolation level 事务隔离 级别;
脏读
一个事务读到另外一个事务还没有提交的数据,称之为脏读。脏读 演示的执行流程如下:
脏读演示步骤1
设置窗口 2 的事务隔离级别为读未提交,设置命令如下:
set session transaction isolation level read uncommitted;
注意: 事务隔离级别读未提交存在脏读的问题。
脏读演示步骤2
窗口2开启事务,查询用户表如下图所示:
start transaction; select * from userinfo;
注意: 从上述结果可以看出,在窗口 2 中读取到了窗口 1 中事务未提 交的数据,这就是脏读。
脏读演示步骤3
在窗口 1 中开启一个事务,并给 Java 账户加 50 元,但不提交事 务,执行的 SQL 如下:
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> update userinfo set balance=balance+50 where name="java"; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
脏读演示步骤4
在窗口 2 中再次查询用户列表,执行结果如下:
注意: 从上述结果可以看出,在窗口 2 中读取到了窗口 1 中事务未提交的数据,这就是脏读。
不可重复读
不可重复读是指一个事务先后执行同一条 SQL,但两次读取到的数据不同,就是不可重复读。不可重复读演示的执行流程如下:
不可重复读演示步骤1
设置窗口 2 的事务隔离级别为读已提交
set session transaction isolation level read committed;
注意: 读已提交可以解决脏读的问题,但存在不可重复读的问题。
不可重复读演示步骤2
在窗口 2 中开启事务,并查询用户表,执行结果如下
不可重复读演示步骤3
在窗口 1 中开启事务,并给 Java 用户添加 20 元,但不提交事务, 再观察窗口 2 中有没有脏读的问题,具体执行结果如下图所示:
从上述结果可以看出,当把窗口的事务隔离级别设置为读已提交, 已经不存在脏读问题了。接下来在窗口 1 中提交事务,执行结果如下图所示:
不可重复读演示步骤4
切换到窗口 2 中再次查询用户列表,执行结果如下:
不可重复读和脏读的区别:
脏读可以读到其他事务中未提交的数据,而不可重复读是读取到了其他事务已经提交的数据,但前后两次读取的结果不同。
幻读
幻读名如其文,它就像发生了某种幻觉一样,在一个事务中明明没 有查到主键为 X 的数据,但主键为 X 的数据就是插入不进去,就像某种幻觉一样。幻读演示的执行流程如下:
幻读演示步骤1
在窗口1和窗口2修改事务隔离级别为可重复读。
set session transaction isolation level repeatable read;
幻读演示步骤2
设置窗口 2 为可重复读,可重复有幻读的问题,查询编号为 3 的用户,具体执行 SQL 如下:
start transaction; select * from userinfo where id=3;
注意:从上述结果可以看出,查询的结果中 id=3 的数据为空。
幻读演示步骤3
开启窗口 1 的事务,插入用户编号为 3 的数据,然后成功提交事务,执行 SQL 如下:
start transaction; insert into userinfo(id,name,balance) values(3,'Spring',100); commit;
幻读演示步骤4
在窗口 2 中插入用户编号为 3 的数据,执行 SQL 如下:
insert into userinfo(id,name,balance) values(3,'Spring',100);
注意: 添加用户数据失败,提示表中已经存在了编号为 3 的数据,且 此字段为主键,不能添加多个。
幻读演示步骤5
在窗口 2 中,重新执行查询:
select * from userinfo where id=3;
注意: 在此事务中查询明明没有编号为 3 的用户,但插入的时候却却 提示已经存在了,这就是幻读。
不可重复读和幻读的区别
二者描述的则重点不同,不可重复读描述的侧重点是修改操作,而 幻读描述的侧重点是添加和删除操作。
MySQL中锁的分类
从本质上讲,锁是一种协调多个进程或多个线程对某一资源的访问 的机制,MySQL使用锁和MVCC机制实现了事务隔离级别。
锁的分类
悲观锁和乐观锁
悲观锁
顾名思义,悲观锁对于数据库中数据的读写持悲观态度,即在整个数据处理的过程中,它会将相应的数据锁定。在数据库中,悲观锁的实现需要依赖数据库提供的锁机制,以保证对数据库加锁后,其他应用系统无法修改数据库中的数据。
注意:
在悲观锁机制下,读取数据库中的数据时需要加锁,此时不能对这些数据进行修改操作。修改数据库中的数据时也需要加锁,此时不能对这些数据进行读取操作。
乐观锁
悲观锁会极大地降低数据库的性能,特别是对长事务而言,性能的损耗往往是无法承受的。乐观锁则在一定程度上解决了这个问题。
注意:
实现乐观锁的一种常用做法是为数据增加一个版本标识,如果是通过数据库实现,往往会在数据表中增加一个类似version的版本号字段。
读锁和写锁
读锁
读锁又称为共享锁,共享锁就是多个事务对于同一数据可以共享一 把锁,都能访问到数据,但是只能读不能修改。
写锁
写锁又称为排他锁,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁, 包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
注意:
需要注意的是,对同一份数据,如果加了读锁,则可以继续为 其加读锁,且多个读锁之间互不影响,但此时不能为数据增加 写锁。一旦加了写锁,则不能再增加写锁和读锁。因为读锁具有共享性,而写锁具有排他性。
表锁、行锁和页面锁
表锁
表锁也称为表级锁,就是在整个数据表上对数据进行加锁和释放锁。典型特点是开销比较小,加锁速度快,一般不会出现死锁,锁定的粒度比较大,发生锁冲突的概率最高,并发度最低。
手动增加表锁
mysql> lock table userinfo read; Query OK, 0 rows affected (0.00 sec) mysql> lock table userinfo write; Query OK, 0 rows affected (0.00 sec)
查看数据表上增加的锁
show open tables;
删除表锁
mysql> unlock tables; Query OK, 0 rows affected (0.00 sec)
行锁
行锁也称为行级锁,就是在数据行上对数据进行加锁和释放锁。典 型特点是开销比较大,加锁速度慢,可能会出现死锁,锁定的粒度最小,发生锁冲突的概率最小,并发度最高。
页面锁
页面锁也称为页级锁,就是在页面级别对数据进行加锁和释放锁。 对数据的加锁开销介于表锁和行锁之间,可能会出现死锁,锁定的粒度大小介于表锁和行锁之间,并发度一般。
间隙锁和临键锁
间隙锁
在MySQL中使用范围查询时,如果请求共享锁或排他锁,InnoDB 会给符合条件的已有数据的索引项加锁。如果键值在条件范围内, 而这个范围内并不存在记录,则认为此时出现了“间隙(也就是 GAP)”。InnoDB存储引擎会对这个“间隙”加锁,而这种加锁机制就是间隙锁(GAP Lock)。
例如,userinfo数据表中存在如下数据。
解释:
此时,userinfo数据表中的间隙包括id为(3,15]、(15,20]、 (20,正无穷]的三个区间。如果执行如下命令,将符合条件的用 户的账户余额增加100元。 update userinfo set balance = balance + 100 where id > 5 and id <16; 则其他事务无法在(3,20]这个区间内插入或者修改任何数据。 这里需要注意的是,间隙锁只有在可重复读事务隔离级别下才 会生效。
临键锁
临键锁(Next-Key Lock)是行锁和间隙锁的组合,例如上面例子中 的区间(3,20]就可以称为临键锁。
MySQL中的死锁问题
什么是死锁
死锁是并发系统中常见的问题,同样也会出现在数据库MySQL的并 发读写请求场景中。当两个及以上的事务,双方都在等待对方释放 已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出 现“死锁”。
Deadlock found when trying to get lock...
举例来说 A 事务持有 X1 锁 ,申请 X2 锁,B事务持有 X2 锁,申请 X1 锁。A 和 B 事务持有锁并且申请对方持有的锁进入循环等待,就造成了死锁。
第一步
打开终端A,登录MySQL,将事务隔离级别设置为可重复读,开启 事务后为userinfo数据表中id为1的数据添加排他锁,如下所示。
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from userinfo; +----+-------+---------+ | id | name | balance | +----+-------+---------+ | 1 | Java | 100.00 | | 2 | MySQL | 200.00 | +----+-------+---------+ 2 rows in set (0.00 sec)
第二步
打开终端B,登录MySQL,将事务隔离级别设置为可重复读,开启事务后为userfinfo数据表中id为2的数据添加排他锁,如下所示。
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from userinfo where id = 2; +----+-------+---------+ | id | name | balance | +----+-------+---------+ | 2 | MySQL | 200.00 | +----+-------+---------+ 1 row in set (0.00 sec)
第三步
在终端A为userinfo数据表中id为2的数据添加排他锁,如下所示。
mysql> select * from userinfo where id =2 for update;
注意: 此时,线程会一直卡住,因为在等待终端B中id为2的数据释放排他锁。
第四步
在终端B中为userinfo数据表中id为1的数据添加排他锁,如下所示。
mysql> select * from userinfo where id =1 for update; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
通过如下命令可以查看死锁的日志信息。
show engine innodb status\G
注意:
通过命令行查看LATEST DETECTED DEADLOCK选项相关的信 息,可以发现死锁的相关信息,或者通过配置 innodb_print_all_deadlocks(MySQL 5.6.2版本开始提供)参数为ON,将死锁相关信息打印到MySQL错误日志中。
如何避免死锁
MySQL事务的实现原理_什么是redo log
MySQL的事务实现离不开Redo Log和Undo Log。从某种程度上 说,事务的隔离性是由锁和MVCC机制实现的,原子性和持久性是 由Redo Log实现的,一致性是由Undo Log实现的。
什么是redo log
redo log叫做重做日志,是用来实现事务的持久性。该日志文件由 两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件 (redo log),前者是在内存中,后者在磁盘中。当事务提交之后会 把所有修改信息都会存到该日志中。
注意:
先写日志,再写磁盘的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging) 技术。
Redo Log刷盘规则
在计算机操作系统中,用户空间( user space )下的缓冲区数据一般情况 下是无法直接写入磁盘的,中间必须经过操作系统内核空间( kernel space )缓冲区( OS Buffer )。因此, redo log buffer 写入 redo log file 实际上是先 写入 OS Buffer ,然后再通过系统调用 fsync() 将其刷到 redo log file 中。
mysql 支持三种将 redo log buffer 写入 redo log file 的时机。 innodb_flush_log_at_trx_commit。