事务就是一组应该一起成功或一起失败的SQL语句。事务还应该具备原子性、一致性、隔离性和持久性简称(ACID)的熟悉。

Atomicity Consistency Isolation Durability = ACID

MySQL 数据库事务介绍

  • 原子性
    • 所有的SQL 语句要么全部成功,要么全部失败,不会存在部分更新。
  • 一致性
    • 事务只能以允许的方式改变受其影响的数据。
  • 隔离性
    • 同时发生的事务(并发事务)不应该导致数据处于不一致的状态。
  • 持久性
    • 无论数据库或系统是否故障,数据都会永久保存在磁盘上,并且不会丢死。

使用SQL 语句控制事务

  1. -- MySQL 启动事务
  2. START TRANSACTION;
  3. BEGIN;
  4. -- 执行事务
  5. -- 提交事务
  6. COMMIT;
  7. -- 回滚事务
  8. ROOLLBACK;
  9. BEGIN;

MySQL Autocommit 状态

-- autocommit 默认是启用状态,表示一旦执行就会被提交,除非该语句在BEGIN...COMMIT块中
-- 关闭autocommit 需要执行 0 表示关闭 1表示开启
SET autocommit = 0;

事务保存点

BEGIN;
-- 执行事务1
-- 保存点
SAVEPOINT transfer_b; 
-- 执行事务2
-- 回滚了事务保存点
ROLLBACK TO transfer_b;
-- 提交了事务
COMMIT;

隔离级别 (非常重要)(正片开始)

隔离级别定义了一个事务与其他事务在资源或者数据修改方面的隔离程度。

-- 修改隔离级别执行如下
SET @@transaction_isolation='READ-COMMITTED';

-- 查看当前的隔离级别
show variables like '%isolation%';
-- OR
select @@global.tx_isolation, @@tx_isolation;

事务隔离级别性能损耗排行 读取未提交 < 读提交 < 可重复读取 < 序列化

四种隔离级别

  • 读取未提交(read uncommitted)

当前事务可以读取另外一个未提交的事务写入的数据,这也称为脏读;也可以理解事务直接无隔离

  • 读提交 (read committed)

当前事务只能读取另一个事务已经提交的数据,这也为未不可重复读取;

  • 可重复读取 (repeatable read)

可重复读是MySQL 默认的事务隔离级别,一个事务通过第一条语句只能看到相同的数据,即时另一个事务已经提交数据。在同一个事务中,读取通过第一次读取建立快照是一致的,除在当前事务更改的数据。这也被称为多版本并发控制
**

  • 可重复读取与幻读

场景:一个事务(同一个read view)在前后两次查询同一范围的时候,后一次查询看到了前一次查询没有看 到的行。幻读专指新插入的行,读到原本存在行的更新结果不算。因为当前读的作用就是能读到所有 已经提交记录的最新值

影响: 1. 会造成一个事务中先产生的锁,无法锁住后加入的满足条件的行
2.产生数据一致性问题,在一个事务中,先对符合条件的目标行做变更,而在事务提交前有新的符 合目标条件的行加入。这样通过binlog恢复的数据是会将所有符合条件的目标行都进行变更的

原因: 行锁只能锁住行,即使把所有的行记录都上锁,也阻止不了新插入的记录。

解决方案: 将两行记录间的空隙加上锁,阻止新记录的插入;这个锁称为间隙锁。
间隙锁与间隙锁之间没有冲突关系。跟间隙锁存在冲突关系的,是往这个间隙中插入一个记录这个 操作;具体参考MySQL锁。

  • 序列化 (seroalizable

通过把选的的行锁起来,序列化可以提供最高级别的隔离。此级别与可重复度类似,在序列化模式中永远保证读
取最新提交的数据。

读未提交适合零事务,或者少事务的场景例如:新闻资讯类、社区、论坛,他们不会存在事务冲突 读提交适合一般事务场景例如:银行转账、电商、金融部分(P2P、现金贷、消费贷) 可重复读取适合对数据/时间要求比较高的场景:财务、金融、计费系统 他们在运行报表时候对原始数据比较看重 序列化适合对并发要求不高但对数据一致性看重场景:银行核心业务

PS: MySQL 只有四种隔离级别, 在各大云平台数据库中,阿里云采用的是“读提交”, 腾讯和华为采用的是“可重复读取”

那我们如何选择, 是否选择MySQL默认隔离级别? MySQL 默认可重复读取是因为在MySQL5.6以前的版本中存在binlog 无法同步问题因为才会默认可重复读取,这个属于MySQL历史包袱,在MySQL 的今天这些或多或少的问题都得到了一定的解决,对比其他数据库Oracle、SQLServer 等数据库它们的默认隔离级别都是读提交也说明了读提交的良好适用性。 上面我也介绍了各个事务的场景,在一般的开发业务中读取提交是比较好的选择,它既能避免脏读也有良好的并发性能,适用的场景也比较符合互联网企业业务

MySQL锁

MySQL存在二种锁,内部锁和外部锁,内部锁又分为行级锁、表级锁。

表锁

开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低

使用LOCK TABLE 和 UNLOCK TABLES 语句控制 READ: 多个会话可以从表中读取数据而不需要获取锁。此外多个会话可以在同一个表上获得锁,这也就是为什么READ 锁被称为共享锁。如果有任何写入尝试,该操作将处于等待状态,直到READ锁被释放。 WRITE: 除了持有锁会话之外,其他任何会话都不能读取或向表中写入数据。这就是为什么WRITE锁被称为排它锁。

-- 锁定表
-- LOCK TABLES table_name [READ | WRITE]
LOCK TABLES table_name READ;

-- 冻结数据库所有的写入操作
FLUSH TABLES WITH READ LOCK;

行锁

开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高

使用 UPDATE 或者 SELECT * FROM 表 FOR UPDATE

-- 必须开启事务
-- 通过查询锁
select * from 表 where a = 2 for update;
-- OR 通过UPDATE 锁 常用, WHERE 必须有命中索引否则会提升到表锁
UPDATE 表 SET balance = balance + 100 WHERE id = 1 AND balance > 0

间隙锁

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内并不存在的记录,叫做间隙锁

-- 通过范围查询进行锁, 时间数据库表中并没有id 在 2~6 之间
update user set count = 8 where id>2 and id<6

锁队列

共享锁(读锁):其他事务可以读,但不能写, 例如: 默认事务。 排他锁(写锁) :其他事务不能读取,也不能写, 例如: 间隙所、行锁。

除共享锁之外没有两个锁可以一起加在一个表上。如果一个表已经有一个共享锁,此时有一个排他锁进来,那么他将保留在队列中,直到共享锁被释放。当排他锁在队列中时,所有后续的共享锁也会被阻塞保留在队列中。

-- 通过查询锁队列
SHOW PROCESSLIST;

悲观锁 OR 乐观锁

乐观锁( Optimistic Locking):顾名思义,对加锁持有一种乐观的态度,即先进行业务操作,不到最后一步不进行加锁,”乐观”的认为加锁一定会成功的,在最后一步更新数据的时候再进行加锁。

悲观锁(Pessimistic Lock):正如其名字一样,悲观锁对数据加锁持有一种悲观的态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

读取频繁使用乐观锁,写入频繁使用悲观锁。乐观锁不能解决脏读的问题。

悲观锁 / 乐观锁 只是一种编程思想用于处理程序的一种业务方式

-- 案例: 商品库存扣减方案; 库存字段采用 unsigned 不会出现负数
-- 乐观锁 直接扣减库存; 如果扣减失败返回异常给程序自行处理
update tb_goods_stock set nums = nums - 1 where goods_id = 6689;

-- 悲观锁
-- 1. 锁定行
select nums from tb_goods_stock where goods_id = 6689 for update;
-- 2. 更新行数据
update tb_goods_stock set nums = nums - 1where goods_id = 6689;
-- 3. 提交数据

PS:在上面的案例中,悲观锁和乐观锁区别在于悲观锁在每次扣减库存都需要先锁定行,然后保证数据能高成功率的更新,而乐观锁就是直接扣减库存,能成功则成功不成功则自行处理。 因此悲观锁并发效率低,乐观锁并发高在一般互联网项目中以及高并发抢购商品中均使用乐观锁