MySQL(六)MySQL事务和锁

一、概述

  背景:我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。

  本质:数据库的多事务并发问题

  应对措施:

  • 事务隔离机制
  • 锁机制
  • MVCC多版本并发控制隔离机制

二、事务及其ACID属性

ACID是什么?

  事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。

  • 原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
  • 一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态
  • 隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
  • 持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

  PS:原子性和一致性类似,原子性主要针对于操作,一致性主要针对数据。

并发事务处理带来的问题

脏写/更新丢失(Lost Update)

  当多个事务选择同一行进行操作,并基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题。

  一句话总结:最后的更新覆盖了由其他事务所做的更新。

-- 库存扣减时,库存在代码里算好然后直接执行sql语句【举个例子原来库存是10】

-- A事务【count = 10 - 1 = 9】:
update t_gds_stock set stock_count = 9;

-- B事务【count = 10 - 5 = 5】:
update t_gds_stock set stock_count = 5;

-- 执行完毕后,库存为5,而正确的库存应该为10 - 1 - 5 = 4 

PS:涉及到类似减库存的这种操作,不要在java代码里面做,而是用数据库来控制。

脏读(Dirty Reads)

  一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并作进一步的处理,就会产生对未提交数据的依赖关系,这种现象被称为“脏读”。此时,如果B事务回滚,A读取的数据就无效了,这不符合一致性要求。

   一句话总结:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。

-- 库存扣减时,下单异常库存回滚

-- 第1步 A事务进行库存扣减后【未commit】:
update t_gds_stock set stock_count = 9;

-- 第2步 B事务读取到A事务更新后的库存,并运算
update t_gds_stock set stock_count = 9 - 5;

-- 第3步 A事务异常并回滚
A:roll back;

-- 执行完毕后,库存为4,而正确的库存应该为10 - 5 = 5

不可重读(Non-Repeatable Reads)

  一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。

   一句话总结:事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性。

-- B、C事务正在修改库存,而A事务在一个事务内多次查询库存的结果不一样

-- 第1步 A事务进行库存查询【查询结果:10】:
select stock_count from t_gds_stock;

-- 第2步 B事务修改库存
update t_gds_stock set stock_count = 10 - 5;

-- 第3步 A事务执行同样的语句【查询结果:5】:
select stock_count from t_gds_stock;

-- 第4步 C事务修改库存
update t_gds_stock set stock_count = 5 - 3;

-- 第5步 A事务执行同样的语句【查询结果:2】:
select stock_count from t_gds_stock;

-- 执行完毕后,A事务一脸懵圈,到底应该用哪个库存呀,相当不好写代码呀!

PS:我们至少需要保证在同一个事务内,同一个数据多次查询的结果是一致的,而真正去操作数据库的时候再用最新的真实数据来更新,这样我们的代码才好控制逻辑【最终一致】

幻读(Phantom Reads)

  一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

  一句话总结:事务A读取到了事务B提交的新增数据,不符合隔离性。

-- A事务进行库存分仓查询,此时B事务新增了一条记录,A事务再次查询的时候查出了B事务新增的数据

-- 第1步 A事务进行库存查询【查询结果:3条记录】:
select * from t_gds_stock;

-- 第2步 B事务新增数据
insert into t_gds_stock VALUES (4,'新增的商品',SYSDATE());

-- 第3步 A事务再次进行库存查询【查询结果:4条记录】:
select * from t_gds_stock;

-- 执行完毕后,A事务一脸懵圈,怎么回事跟刚才的查询结果不一样,有点慌啊...

事务隔离级别

  MySQL默认事务级别:repeatable-read【可重复读】

  “脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,需要由数据库提供一定的事务隔离机制来解决。

  PS:数据库的事务隔离越严格,并发影响越小,但性能开销也就越大

-- 查看当前数据库的事务隔离级别【8.0+版本】【下面2条语句都可以查】
select @@transaction_isolation;
show variables like 'transaction_isolation';

-- 设置read uncommitted级别:
set session transaction isolation level read uncommitted;

-- 设置read committed级别:
set session transaction isolation level read committed;

-- 设置repeatable read级别:
set session transaction isolation level repeatable read;

-- 设置serializable级别:
set session transaction isolation level serializable;

PS:Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别。

三、数据库锁

  定义:锁是计算机协调多个进程或线程并发访问某一资源的机制。

锁的分类

从性能上分:乐观锁悲观锁

-- 乐观锁【用一个字段version来标示版本号,更新的时候需要带上并对比】
update t_gds_stock set stock_count = 5 where version = 1;

-- 悲观锁【for update语句】
-- 1、开启事务
begin; 
-- 2、for update 语句【此时会上锁,其他事务要操作的时候,就会暂时阻塞】
select stock_count from t_gds_stock for update;

从对数据库操作的类型分:读锁写锁

  • 读锁共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响【别的事务可以读,不可以写】
  • 写锁排它锁,X锁(eXclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁。【不让别的事务读写】

PS:读锁和写锁都属于悲观锁。

从对数据操作的粒度分:表锁行锁

表锁

  定义:每次操作锁住整张表

  特点:开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低。

  PS: 一般用在整表数据迁移时,不让业务进行数据变更的场景。

-- 手动增加表锁【lock table t_user read/write; 】
lock table t_user read; 
-- 查看表上加过的锁 
show open tables; 
-- 删除表锁 
unlock tables;

行锁

  定义:每次操作锁住一行数据

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

  PS: InnoDB与MyISAM的最大不同有两点:

  • InnoDB支持事务
  • InnoDB支持行级锁

小结

  表锁:InnoDB与MyISAM都有

  行锁:InnoDB有,MyISAM没有

MyISAM
  • 在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。【读写都加锁】
InnoDB
  • 在执行查询语句SELECT时(非串行隔离级别),不会加锁。
  • 但是update、insert、delete操作会加行锁。

  简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。

四、间隙锁(Gap Lock)

  定义:间隙锁,锁的就是两个值之间的空隙。

  如上图,间隙区间有:(3,10)、(10,15)、(15,+无穷)

场景分析

案例解析

  在事务1下面执行

-- 开启事务
BEGIN;
-- 更新数据【未提交】【PS:注意此时锁的区间是(3,15)这个范围】
update t_user set name ='ZH666' where id > 4 and id < 12;

  则其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(3,15)区间都无法新增修改数据。

   PS:间隙锁是在可重复读隔离级别下才会生效。

五、使用锁时的一些注意事项

无索引行锁会升级为表锁

   锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁。

-- 事务1:【name字段没有加索引】
begin;
update t_user set name ='有梦想的肥宅' where name='ZH';


-- 事务2
对表的任一操作都会被阻塞【写操作】

  InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。

  PS:作为条件时该索引不能失效,否则都会从行锁升级为表锁。

事务级别尽量不要设置可串行化

  事务在可串行化级别下,连select语句也会枷锁,效率非常低,一般我们用默认的级别【可重复读】就够了,在更新数据的时候再用sql的真实值去更新就好了。

尽可能的避免join语句

  一定要尽可能的避免join语句,首先join语句非常难优化,一般的互联网公司也是禁止这样玩的。

  一旦使用了Join语句,性能上面非常难以优化,且后期如果需要分库分表也会有很大的问题。

一些锁的优化建议

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

六、小问答

Q:为什么行锁开销大,加锁慢,而表锁则相反呢?

  表锁只需要通过简单的lock语句就可以对一张表上锁了,而行锁还需要定位到对应的行才可以进行上锁。

Q:如何手动开启事务?【默认是自动commit的】 

-- 1、显式声明开启事务
begin;

-- 2、输入需要执行的sql语句
update t_user set name ='有梦想的肥宅' where name='ZH';

-- 3、提交事务
commit; 
原文地址:https://www.cnblogs.com/riches/p/14915644.html