今天
今天做了一个订单状态数据修改的需求,主要工作点是要避免并发修改导致数据的错误不一致,
当前这个模块的技术栈,需要用到MySQL行级锁和JPA的锁规范。
MySQL 行级锁
行级锁是粒度比较小的一种锁,它可以减少许多冲突,因为他只要锁住一行,而不是一张表。
(MySQL的行级锁需要使用INNODB引擎来支持,MyISAM引擎只支持表级锁)
首先回顾一下大学的基础知识:
数据库的锁,是事务在某个数据对象(如表、行等)进行操作前,对数据库发起请求,对其加锁,
使其不被其他事务所修改。
基本的锁有两种:读锁和写锁。
写锁,也称排它锁(ExclusiveLock,也称X锁),若事务对数据D加了X锁,则其他事务不能读取和修改D。
读锁,也称共享锁(ShareLock,也称S锁),若事务对数据D加了S锁,则其他事务只能对D加S锁,不能修改D。
S LOCK 在MySQL:
从上面的测试中可以发现,事务1(窗口1)加了S锁后,事务2不能加X锁和进行修改。
如果S锁一段时间后仍未释放就会超时,如下图:
X LOCK 在MySQL:
从上面的测试中可以发现,事务1加了X锁之后,事务2不能加S锁,但只要去掉LOCK IN SHARE MODE
就可以查询了。
其他的场景,可用以上SQL测试验证一下。
(注意:当用到索引时,MySql才会使用row-lock,否则table-lock。)
JPA 规范
看完了锁在MySQL的实现,那在Java中如何使用呢?这里以spring+JPA为例(为了方便,使用了starter-jpa):
JPA 锁相关规范
在spring-data-jpa中,@Lock + LockModeType
可以为某个数据操作进行锁控制,比如用X锁:
@Lock(LockModeType.PESSIMISTIC_WRITE) |
可以跑一下测试:
- 使用上面的窗口二先加S锁,不commit。
- 然后调用findByOrderNo()查询。
- 可以发现,程序抛出LockAcquisitionException异常,不能获取锁,等待超时,如下:
Caused by: org.hibernate.exception.LockAcquisitionException: could not extract ResultSet |
现在来看Lock和LockModeType,Lock是spring-data的annotation,
LockModeType属于JPA规范,可作为EntityManager的方法参数传入或者使用Query.setLockMode()或TypedQuery.setLockMode()进行设置。
当然使用spring-data的话,直接使用@Lock即可,如果对细节感兴趣,可以去看spring-data的源码。
LockModeType是一个enum,它包括:
READ:同OPTIMISTIC |
上面涉及到了悲观锁和乐观锁:
所谓悲观锁,就是对数据被其他事务修改的概率保持悲观态度,
因此在处理过程中,都将数据锁定。
而乐观锁,反过来即是保持乐观态度,认为数据被其他事务修改的概率是较低的,所以在真正提交更新的时候,
才去检测数据是否冲突,实现方式可以是版本号或者时间戳,这里不再赘述。
悲观锁能严格保证数据的正确性,但凡事有利必有弊,很多事情我们都需要去平衡。
如果一个并发冲突的概率不高,而使用悲观锁,会对数据库性能开销影响比较大。
除此之外,良好的设计也可有效地避免一些并发问题。
参考资料
【1】 https://spring.io/guides/gs/accessing-data-jpa/
【2】 http://docs.oracle.com/javaee/6/tutorial/doc/gkjiu.html
【3】 https://dev.mysql.com/doc/refman/5.5/en/innodb-lock-modes.html