社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
首先看下数据库事物四大特性,ACID,原子性,一致性,隔离性,持久性。
隔离性:由并发事务所作的修改必须与任何其它并发事务所作的修改隔离,互相不受影响。同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。mysql具有四种事物隔离级别,隔离力度依次递增,高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。不同业务场景下使用不同的数据库事物隔离性,部分关键业务采用隔离性高的隔离级别,以保证数据正确性。
脏读:在隔离级别读未提交中可能会出现,一个事务读取另外一个事务还没有提交的数据叫脏读,事物A读取了事物B更新的事物,事物B没有commit并且回滚,此时就事物A产生脏读,应用也没保证数据的正确性。
create table account(id int(11) primary key auto_increment,name varchar(16),money float);
mysql默认的事物隔离级别为可重复读(Repeatable Read),可使用SELECT @@tx_isolation查看。
设定事物隔离级别,mysql存储引擎InnoDB支持事物,MyISAM不支持事物,当前mysql默认存储引擎是InnoDB。
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE];
这里,我们开启两个mysql的session会话,把隔离级别都设定为读未提交:
向表account中插入一条记录,zhangsan金额为1000
开启一个事物A,在该事物中把zhangsan的金额扣除300,暂时不提交和回滚:
begin;
update account set money=money-300 where id=1;
另开一个事物B,在事物B中查看zhangsan当前金额数:
begin;
select * from account where id=1;
此时在事物B中读取到的数据就为脏数据,事物B读取到了事物A未提交(commit)的数据,当事物A回滚(rollback)时,事物B之前读取到的数据会给应用带来数据错误。
不可重复读:在隔离级别读已提交中可能会出现的问题,事物能读取其他事物对同一数据的更改。事务 A 多次读取同一数据,事务 B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
set session transaction isolation level read committed;
在A中开启一个事物A,第一次读取到zhangsan的金额数为1000:
begin;
select * from account where id=1;
然后另外开启一个事物B,添加金额200,并且提交事物:
begin;
update account set money=money+200 where id=1;
commit;
在事物B提交之后再在事物A中读取zhangsan的金额数:
可以看到事物A读取到的数据与之前读取到的不一致,此时就产生了不可重复读的现象。
幻读:在隔离级别可重复读中可能出现的问题,幻读是针对按范围读取多条数据的现象。同一事务A多次查询,若另一事务B只是update,则A事务多次查询结果相同;若B事务insert/delete数据,则A事务多次查询就会发现新增或缺少数据,出现幻读。
set session transaction isolation level repeatable read;
在事物A中查找表数据:
然后在B中开启新事物,插入一条数据,并提交事物:
begin;
insert into account value(5,'l5',500);
commit;
接下来我们在事物A中查询,发现查询不到第5条记录,但是我们在A中插入id为5的记录时会出问题:
此时就产生了幻读现象,在事物A中查询不到数据,却被告知数据已经存在,解决幻读一般需要锁表。
综上,四种隔离级别分别会出现的问题,读未提交未解决任何问题,串行化避免了所有的问题,mysql默认可重复读:
mysql默认存储引擎是InnoDB,InnoDB与Myisam相比,InnoDB支持事物,支持多种锁机制,有行锁和表锁,Myisam只支持表锁,且不支持事物。
上述详细的介绍了mysql事物隔离级别,那么,mysql事物隔离是怎么实现的呢?底层原理是什么?
锁是用来实现数据库事物隔离级别的重要机制,mysql提供了多种锁机制,有共享锁、排他锁、意向共享锁、意向排他锁等,其中共享锁和排他锁都是行锁,意向锁是表锁,意向锁系统控制,人为操作不了。
数据库实现事务隔离的方式有两种:
在mysql中,锁分为表锁、行锁,行级锁有共享锁、排他锁。表锁有意向锁,意向共享锁和意向排他锁。
在可重复读中,幻读是通过什么方式解决的?
innoDB存储引擎的锁的算法有三种:
Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生。原理是对行数据操作时,gap锁会对相邻范围的行数据加锁,阻塞,直到操作完成,针对的是非唯一性索引数据。如果是有索引的数据则会使用recored lock行锁,相对来说锁的范围较小,阻塞程度小。
为什么要存在意向锁?
innodb的意向锁主要用户多粒度的锁并存的情况。比如事务A要在一个表上加S锁,如果表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。如果表中的数据很多,逐行检查锁标志的开销将很大,系统的性能将会受到影响。举个例子,如果表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B需要给这个表加表级锁,如果没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。如果存在意向锁,那么假如事务A在更新一条记录之前,先加意向锁,再加X锁,事务B先检查该表上是否存在意向锁,存在的意向锁是否与自己准备加的锁冲突,如果有冲突,则等待直到事务A释放,而无须逐条记录去检测。
乐观锁、悲观锁
锁从使用方式来分可分为乐观锁和悲观锁,乐观锁和悲观锁在很多应用当中都存在的概念,并不是实质存在的锁叫乐观锁悲观锁,在mysql数据中有,在hibernate、java等当中也有。
乐观锁是在遇到事物并发问题时,想法很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,认为这次的操作不会导致冲突,当出现事物并发问题时再处理。乐观锁由于加锁少,所以性能开销比较小,吞吐量大。
悲观锁的特点是先获取锁,再进行业务操作。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,开始就默认会出现事物并发问题,所以在进行每次操作数据时都要通过获取锁才能进行对相同数据的操作,悲观锁需要耗费较多的时间,处理并发问题也相对严谨,在核心业务的关键之处可使用悲观锁,开销相对来说大。
平时在购买商品操作库存流程大概如下:
假如当前库存量为5,第一个购买请求数量为4个,顺利购买,当前剩余1,第二次购买4个,此时库存不足,程序直接返回库存不足,购买失败。
上述情况不是在高并发情况下,在高并发情况下,可能有多个请求同时购买,同时读取库存更新存库,两次请求同时读取到的库存都是5,然后都执行购买操作,减库存,就出现了库存超发现象,库存减至-3。
类似问题可以用乐观锁、悲观锁来解决。
1、悲观锁解决方案
出现超发的问题根本原因就是共享的数据被多个线程同时修改,如果单独的一个线程想要修改数据,在读取数据时将数据锁定,加排他锁,不允许其他线程读取和修改数据,直到存库修改完成释放锁,就不会出现超发现象。
//开始事物
select id,productid,stock from zproduct where id=? for update;
update zproduct set stock=stock-? where id=?;
//结束事物
使用for update给行数据加排他锁,其他事物就不能对它读和写,避免了数据同时被多个事物修改,此解决方案是实现比较简单,缺点是加排他锁影响系统的并发性能。
乐观锁解决方案
2、乐观锁解决方案
悲观锁很容易产生线程阻塞,为了提高性能,出现了乐观锁方案,不使用数据库锁,不阻塞线程并发。
实现方法:给商品添加一个version字段,代码行修改版本号,读取库存是拿到这个version版本号,在更新时再对比version版本号,如果版本号相同,说明库存没有被其他线程修改过,可以正常操作,同时把version数值加1。如果版本号不同,代表被别的线程修改过,则取消修改库存操作,返回购买失败。
update zproduct set stock=stock-?,version=version+1 where id=? and version=?;
为进一步完善,可以借鉴中间件的重试机制,更新失败后,重新来一遍,重新读取、修改,超过一定时间一定重试次数后,还不成功就放弃,返回失败,否则成功。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!