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

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

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

并发事务可能引发的问题

####1.数据丢失

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s8YPOXSE-1604600685899)(C:\Users\wl\Desktop\学习\Java\md\数据库\gbf_img\db7d28a1f27d46cf534064ab4e74f47d.jpg)]

####2.脏读、

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XQUsSK1-1604600685902)(C:\Users\wl\Desktop\学习\Java\md\数据库\gbf_img\d717c7e782620d2e46beb070dbc8154c.jpg)]

####3.幻读

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CClJShGt-1604600685905)(C:\Users\wl\Desktop\学习\Java\md\数据库\gbf_img\6173739ee9a5d7e26c8b00f2ed8d9e9a.jpg)]

####4.不可重复读

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0zrcRP2t-1604600685908)(C:\Users\wl\Desktop\学习\Java\md\数据库\gbf_img\280826363e1d5a3e64529dfd3443e5b6.jpg)]

事务隔离解决的并发问题

​ 数据丢失可以基于数据库中的悲观锁来避免发生,即在查询时通过在事务中使用 select xx for update 语句来实现一个排他锁,保证在该事务结束之前其他事务无法更新该数据。
​ 我们也可以基于乐观锁来避免,即将某一字段作为版本号,如果更新时的版本号跟之前的版本一致,则更新,否则更新失败。剩下3 个问题,其实是数据库读一致性造成的,需要数据库提供一定的事务隔离机制来解决。

MySQL 的锁机制

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

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

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

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

锁的具体实现算法

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GEjHMM6d-1604600685910)(gbf_img\0c60d5685aa881cf66be43c6c4529927.jpg)]

这两种不同的执行方式,虽然这些操作都在一个事务中,但是锁的申请不在同一时间,锁只有当其他操作都执行完成才会释放锁。扣减库存是更新操作,属于行锁,如果先扣减库存会影响到其他操作该数据的事务,所以我们应该尽可能的避免长时间持有该锁,尽快的释放锁。

因为创建订单和扣除库存不管先执行哪一步都不影响业务,所以我们可以先执行新增操作,把扣除库存放到最后,也就是使用执行顺序1 ,来减少锁的持有时间。

总结

MySQL 的并发事务调优和 Java 的多线程编程调优非常类似,都是可以通过减小锁粒度和减少锁的持有时间进行调优。在 MySQL 的并发事务调优中,我们尽量在可以使用低事务隔离级别的业务场景中,避免使用高事务隔离级别。

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

故里学Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值