高并发场景下的数据库事务调优

数据库事务是访问可能操做各类数据项的一个数据库操做序列,这些操做要么所有成功,要么所有失败。提起事务,你们都知道ACID属性,这些特性在前边的文章里都有详细的讲解,感兴趣的能够经过历史文章查看。在Java中有并发编程,能够多线程并发执行,并发能够提升程序执行的效率,也会带来线程安全的。数据库事务和多线程同样,为了提升数据库处理事务的吞吐量,数据库也支持并发事务,在并发处理数据的过程当中,也存在着安全问题。
算法

咱们本文将从并发事务可能引起的问题、解决并发问题、MySQL的锁机制、锁的实现等方面逐渐深刻,探讨高并发场景下的事务调优问题。数据库

并发事务可能引起的问题

1.数据丢失

图片

2.脏读、

图片

3.幻读

图片

4.不可重复读

图片

事务隔离解决的并发问题

数据丢失能够基于数据库中的悲观锁来避免发生,即在查询时经过在事务中使用 select xx for update 语句来实现一个排他锁,保证在该事务结束以前其余事务没法更新该数据。 咱们也能够基于乐观锁来避免,即将某一字段做为版本号,若是更新时的版本号跟以前的版本一致,则更新,不然更新失败。剩下3 个问题,实际上是数据库读一致性形成的,须要数据库提供必定的事务隔离机制来解决。编程

MySQL 的锁机制

InnoDB实现了两种类型的锁机制:共享锁(S)和排他锁(X)。共享锁容许一个事务读数据,不容许修改数据,若是其余事务要再对该行加锁,只能加共享锁;排他锁是修改数据时加的锁,能够读取和修改数据,一旦一个事务对该行数据加锁,其余事务将不能再对该数据加任务锁。安全

不一样的锁机制会产生不一样的事务隔离级别,不一样的隔离级别分别能够解决并发事务产生的问题,如读未提交、读已提交、可重复读、可序列化等。(1号发的《MySQL的事务隔离级别和长事务,看这一篇就够了》一文中有介绍过)多线程

InnoDB中的读已提交和可重复读隔离事务是基于多版本并发控制(MVCC)实现高性能事务。一旦数据被加上排他锁,其余的事务将没法加入共享锁,且处于阻塞等待状态,若是一张表有大量的请求,这样的性能将是没法支持的。并发

MVCC对普通的Select 不加锁,若是读取的数据正在执行delete或者update操做,这时读取操做不会等待排他锁的释放,而是直接利用MVCC读取该行的数据快照。MVCC避免了对数据重复加锁的过程,大大提升了毒草在的性能。(数据快照是指在该行的以前版本的数据,而数据快照的版本是基于undo实现的,undo是用来作事务回滚的,记录了回滚的不一样版本的行记录)ide

锁的具体实现算法

InnoDB既实现了行锁,也实现了表锁,行锁是经过索引实现的,若是不经过索引条件检索数据,那么InnoDB将表中全部的记录进行加锁,其实就是升级为表锁。高并发

行锁的具体实现算法有三种:record lock、gap lock和next-key lock。record lock是专门对索引项加锁;gap lock是对索引项之间的间隙加锁,next-key lock则是前面两种的组合,对索引项及其之间的间隙加锁。性能

只在可重复读或以上隔离级别下的特定操做才会取得 gap lock 或 next-key lock,在 Select 、Update 和 Delete 时,除了基于惟一索引的查询以外,其余索引查询时都会获取 gap lock 或 next-key lock,即锁住其扫描的范围。优化

优化高并发事务

上边的讲解,都是为了对事务、锁和隔离级别更加深刻了解,下边将聊聊高并发场景下的事务是如何调优的。

  1. 结合业务场景,使用低级别事务隔离

在高并发业务中,为了保证业务数据的一致性,操做数据库时每每会使用不一样级别的事务隔离,隔离等级越高,并发性能就越低。

那在实际的业务中,咱们要如何选择呢,下边举两个例子:

在修改用户的最后登陆时间,或者用户的我的资料等数据时,这些数据都只有用户本身登陆和登录后才会修改,不存在一个事务提交的信息被覆盖的可能,因此这样的业务咱们就最低的隔离级别。

若是帐户的余额或者积分的消费,就可能存在多个客户端同时消费一个帐户的状况,此时咱们应该选择可重复读隔离级别,来保证当一个客户端在操做的时候,其余客户端不能对该数据进行操做。

  1. 避免行锁升级表锁

咱们知道,InnoDB中行锁是经过索引实现的,当不经过索引条件检索数据时,行锁就会升级成表锁,咱们知道表锁会严重影响咱们对整张表的操做,应该避免这种状况。

  1. 控制事务的大小,减小锁定的资源和锁定的时间

下边这个SQL异常相比不少并发比较高的系统里都会碰见,好比抢购系统的日志中:

MySQLQueryInterruptedException: Query execution was interrupted

因为抢购系统中,提交订单业务开启了事务,在并发环境中对一条记录进行更新操做的状况下,因为更新记录所在的事务还可能存在其余操做,致使一个事务比较长,当大量请求进入时,就可能致使一些请求同时进入事务中,因为锁的竞争是不公平的,当多个事务同时对一条记录进行更新时,极端状况下,一个更新操做进去排队系统后,可能会一直拿不到锁,最后因超市被系统中断,就会抛出上边这个异常。

提交订单须要建立订单和扣减库存,两种不一样顺序的执行方式,结果都同样,可是性能确实不同的:

图片

这两种不一样的执行方式,虽然这些操做都在一个事务中,可是锁的申请不在同一时间,锁只有当其余操做都执行完成才会释放锁。扣减库存是更新操做,属于行锁,若是先扣减库存会影响到其余操做该数据的事务,因此咱们应该尽量的避免长时间持有该锁,尽快的释放锁。

由于建立订单和扣除库存无论先执行哪一步都不影响业务,因此咱们能够先执行新增操做,把扣除库存放到最后,也就是使用执行顺序1 ,来减小锁的持有时间。

总结

MySQL 的并发事务调优和 Java 的多线程编程调优很是相似,都是能够经过减少锁粒度和减小锁的持有时间进行调优。在 MySQL 的并发事务调优中,咱们尽可能在可使用低事务隔离级别的业务场景中,避免使用高事务隔离级别。

在功能业务开发时,咱们每每会为了追求开发速度,习惯使用默认的参数设置来实现业务功能。例如,在 service 方法中,你可能习惯默认使用 transaction,不多再手动变动事务隔离级别。但要知道,transaction 默认是 RR 事务隔离级别,在某些业务场景下,可能并不合适。所以,咱们仍是要结合具体的业务场景,进行考虑。

相关文章
相关标签/搜索