开始之前
本文主要讲解各种常见锁策略的应用,希望通过这种实例讲解能让大家更清晰的理解各种锁的区别,在实际项目的该如何选择。由于本文的代码例子使用java编写,涉及一些java框架如Spring JPA等,建议对java熟悉的人读。
实例
本文以电商中一个常见的场景作为演示,如下图
我们的商品有个库存数量的字段,下单的时候系统检查库存是否足够,如果满足则库存数量减少,然后返回下单成功。
代码1:商品下单更新库存部分简写
@Transactional public String doOrder(String goodsId, intbuyCount) throws Exception{ //获取当前商品的库存数量 LjdpWhStock stock = repository.getOne(goodsId); if(stock.getStockNum() >= buyCount) { //如果库存数量大于下单的数量,下单成功,更新库存数量 stock.setStockNum(stock.getStockNum()-buyCount); repository.save(stock); return"下单成功"; } thrownew Exception("下单失败:库存不足"); } |
我们可以把上面的代码简单理解为如下三步曲:
第一步:获取最新库存数量
第二步:判断库存数量是否足够
第三步:更新库存数量
测试结果1(无并发控制)
目前我们的代码中还没有使用任何的并发控制,现在假设有2个线程同时并发下单,可能会出现下面的情况:
(为了便于理解,先假设某商品库存当前剩余数量=1,两个并发线程同时购买同一商品数量=1) Thread-1:查询最新库存剩余数量=1 Thread-2:查询最新库存剩余数量=1 Thread-1:判断库存数量1>=购买数量1,允许下单 Thread-2:判断库存数量1>=购买数量1,允许下单 Thread-1:更新库存数量=0,提交事务,下单成功 Thread-2:更新库存数量=0,提交事务,下单成功 |
可以发现虽然商品只剩一个库存,但是两个线程都下单成功了的诡异现象!这是程序在并发处理中常见的bug,对于这种数据库并发访问的情况,常见的处理方法有:设置事务隔离级别、使用锁,锁又分为乐观锁、悲观锁、共享锁、排他锁等
方案一:使用隔离级别:SERIALIZABLE
设置事务隔离级别为:SERIALIZABLE,串行化,其实就是不允许并发更新,所以勉强也可以解决问题…
例如java常用的spring框架可以通过下面方式设置
<!-- aop事务属性设置 --> <tx:advice id="txAdvice"> <tx:attributes> <tx:method name="*" read-only="true" /> <tx:method name="do*" propagation="REQUIRED" isolation="SERIALIZABLE" rollback-for="Exception" /> </tx:attributes> </tx:advice> |
重启服务后,按照上面的并发再次测试:
测试结果2(使用SERIALIZABLE事务隔离级别)
Thread-1:查询最新库存剩余数量=1 Thread-2:查询最新库存剩余数量=1 Thread-1:判断库存数量1>=购买数量1,允许下单 Thread-2:判断库存数量1>=购买数量1,允许下单 Thread-1:更新库存数量=0,提交事务,下单成功 Thread-2:更新库存数量=0,提交事务,抛异常,事务回滚(经测试不同的jdbc驱动抛的Exception不一样) #下面是mysql-5.1.36的报错信息 Thread-2:[SqlExceptionHelper] Deadlock found when trying to get lock; try restarting transaction Thread-2:[SqlExceptionHelper] SQL Warning Code: 1213, SQLState: 41000 #下面是Oracle11.2g的报错信息 Thread-2:[SqlExceptionHelper] ORA-08177: 无法连续访问此事务处理 Thread-2:[SqlExceptionHelper] SQL Error: 8177, SQLState: 72000 |
这次数据正常了,只能有一个线程下单成功了,其他并发线程会抛出异常,事务提交失败原因不同数据库提供商的描述不一样,我理解是“数据已经被其他事务更新了,需要重启事务”,到此程序的并发bug总算解决了,虽然抛异常很不爽,而且这样系统的并发处理能力非常低,一般不推荐。
方案二:使用乐观锁(Optimistic locking)
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,如果数据已经被其他人更新了,那么表示当前的数据是个过期数据,拒绝更新。
乐观锁常规演示
回到上面的例子,我在库存表LjdpWhStock里加个版本字段(version),每次更新时版本号加1,并且更新时加上条件版本号必须和当前一致,否则将更新失败
代码2:乐观锁版本
@Transactional public String doOrder (String goodsId, intbuyCount) throws Exception{ //获取当前商品的库存数量 LjdpWhStock stock = repository.getOne(goodsId); if(stock.getStockNum() >= buyCount) { intnewNum = stock.getStockNum() - buyCount; //如果库存数量大于下单的数量,下单成功,更新库存数量 intc = (int)executeSQL( "udpate LjdpWhStock t set t.stockNum="+newNum +", t.version=t.version+1 " + "where t.stockId=? and t.version=?" , stock.getStockId(), stock.getVersion()); if(c == 0) { thrownew Exception("下单失败:数据过期"); } return"下单成功"; } thrownew Exception("下单失败:库存不足"); } |
如果update的sql返回0,说明数据已经被其他事务更新了,返回失败提示
重启后再次测试并发,变成以下这样
测试结果3(使用乐观锁)
Thread-1:查询最新库存剩余数量=1 Thread-2:查询最新库存剩余数量=1 Thread-1:判断库存数量1>=购买数量1,允许下单 Thread-2:判断库存数量1>=购买数量1,允许下单 Thread-1:更新库存数量=0,version加1,提交事务,下单成功 Thread-2:更新库存数量=0,version加1,提交事务,抛异常“数据过期”,回滚 |
现在相比SERIALIZABLE隔离级别,系统的并发能力提升了,但是“数据过期”这种错误不可能直接抛给用户,对用户来说唯一能理解的错误是“商品没有货”,解决的方法是遇到并发异常时进行重试直到更新成功或判断库存为0结束,参考下面的代码我使用“递归重试”的方式
代码3:乐观锁版本(错误递归重试)
//其他代码一样,只看优化的这部分 if(c == 0) { //更新失败后递归重试 return doOrder(goodsId, buyCount); } |
在这个版本下面系统要么返回下单成功,要么返回“库存不足(没货了)”,但是使用递归需要注意一些问题:
注意1:检查数据库隔离级别的设置
你需要检查下当前数据库隔离级别的设置,如果隔离级别为REPEATABLE_READ,那么不管你重试多少次读到的都是第一次读到的版本,并不能获取数据库中当前最新的版本,程序建会无限递归直到内存栈溢出报错:java.lang.StackOverflowError
由于不同的数据库对隔离级别支持不一样,我在使用Oracle11g时设置REPEATABLE_READ会返回不支持,默认的是READ_COMMITTED所以不存在这个问题。然后我改用mysql-5.1.36,发现竟然支持REPEATABLE_READ,在REPEATABLE_READ模式下测试就会无限递归直到StackOverflowError
在JPA中使用乐观锁
JPA支持两种乐观锁策略
1.LockModeType.Optimistic
这是默认的锁类型,当在实体中添加@Version注解后,将自动使用此类型,具体实现相当于我上面的例子。
2.LockModeType.Optimistic_FORCE_INCREMENT
当提交事务时,不管实体的状态是否有变更,都将增加版本号。如果你希望另一个实体锁住当前实体的引用时,就需要用它了。也就是说你的事务需要引用当前实体,虽然不会修改它,但也不希望在用的过程中被其他事务修改。
例如我们有个实体“书(Book)”,现在需要把它移动到一个“书架(Shelf)”,为了防止被同时移动到不同的“书架(Shelf)”,在移动的过程中需要对“书(Book)”加锁。
回到本文的【商品下单减少库存】例子中,我们并不需要这个特性,不过我也进行一次实验看实际效果来验证官方的解析。
首先:由于这次使用JPA帮忙实现锁机制,所以代码继续使用【代码1】的代码,然后设置强制使用Optimistic_FORCE_INCREMENT模式,不然默认会用Optimistic,首先在我们的JPA接口中增加如下注解:
代码4:设置锁模式 Optimistic_FORCE_INCREMENT
//获取库存对象,指定乐观锁模式 @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) public LjdpWhStock getOne(String id); |
接着:我把当前测试商品的库存数量设置为0,这样下单不会成功,也不需要更新库存。
如上图:当前测试商品的库存为0,version=10
然后进行一次下单操作看看,结果当然和预期一样返回下单失败,这时打开服务器的日志看(下图):
系统自动对版本号进行了更新,在打开商品库存表看看:
库存还是0没变,但版本号更新为11了。
方案三:使用悲观锁(Optimistic locking)
当事务需要修改一个可能被其他事务同时修改的实体时,事务会发起一个命令将实体锁住。所有的锁会持续到事务结束后再自动释放。悲观锁通常使用数据库中的锁机制实现,数据库中的锁机制又分为共享锁(S)和排它锁(X),分别对应JPA中常用的两种悲观锁策略:PESSIMISTIC_READ和PESSIMISTIC_WRITE
JPA悲观锁之读锁
3.LockModeType.PESSIMISTIC_READ
意思是:事务可以并发读取实体,但不能并发更新。
还是继续使用【代码1】,只是把锁策略更改为PESSIMISTIC_READ,使用同样的并发测试用例测试结果如下:
测试结果4(使用锁策略:PESSIMISTIC_READ)
Thread-1:查询最新库存剩余数量=1(获取共享锁) Thread-2:查询最新库存剩余数量=1(获取共享锁) Thread-1:判断库存数量1>=购买数量1,允许下单 Thread-2:判断库存数量1>=购买数量1,允许下单 Thread-1:更新库存数量=0,提交事务,下单成功 Thread-2:更新库存数量=0,提交事务,抛数据库级别异常,事务回滚 #下面是mysql-5.1.36的报错信息 Thread-2:[SqlExceptionHelper] Deadlock found when trying to get lock; try restarting transaction Thread-2:[SqlExceptionHelper] SQL Warning Code: 1213, SQLState: 41000 |
这个测试结果说明在这种锁策略下也能成功防止并发更新bug,当事务更新一个已经被其他事务更新了的数据时,数据库将返回提交失败的信息。
在mysql中与方案一相比可以发现,mysql返回的错误信息完全一样,个人理解是在mysql中SERIALIZABLE隔离级别实现相当于自动对所有查询获取共享锁,而我们使用锁策略是只对指定的查询获取锁。
另外由于我并不清楚oracle11g(时间关系我只安装了这个版本)如何实现行级共享锁,所以并没能测试成功,我对oracle没有深入研究,希望这方便牛人帮忙解答下。
JPA悲观锁之写锁
4.LockModeType.PESSIMISTIC_WRITE
意思是:事务即不能并发读实体,也不能并发更新实体。
还是继续使用【代码1】,只是把锁策略更改为PESSIMISTIC_WRITE,使用同样的并发测试用例测试结果如下:
测试结果5(使用锁策略:PESSIMISTIC_WRITE)
Thread-1:查询最新库存剩余数量,取得【排他锁】,返回数量=1 Thread-2:查询最新库存剩余数量,尝试获取【排他锁】被阻塞(等待中)… Thread-1:判断库存数量1>=购买数量1,允许下单 Thread-1:更新库存数量=0,提交事务,释放【排他锁】,下单成功 Thread-2:成功获取排他锁,查询最新库存剩余数量=0 Thread-2:判断库存数量0>=购买数量1,为false, 下单失败,返回“库存不足” |
这种锁策略在查询时就把记录(行级)锁住,直到事务提交后释放,其他事务也只能在成功获取锁后才能查询同一行记录,所以有效的防止了并发bug。并且由于不会抛出异常,也简化了程序的处理逻辑。
总结
不同的锁策略有不同的应用场景,需要综合考虑系统对并发性能的要求,和当发生并发异常(获取锁失败)时的处理策略等,在本文的例子中悲观(写)锁策略是比较合适的,由于使用了行级锁,所以也不会阻塞其他商品的下单(需要注意mysql中只有索引才能使用行级锁,不然将变成锁表),但由于同一商品大量并发下单时很可能会阻塞部分请求,当积累大量请求被阻塞时也可能导致服务器挂了,
可以通过设置一个事务超时时间(或者锁超时时间)防止请求被长时间阻塞。
另外悲观锁和乐观锁也可以同时使用,例如某个数据可能会被多个不同的业务同时并发修改,不同业务对并发的要求不一样,可以同时使用悲观锁+乐观锁,在JPA中也定义了这样一种锁策略叫:PESSIMISTIC_FORCE_INCREMENT
参考资料:
https://openjpa.apache.org/builds/2.2.2/apache-openjpa/docs/jpa_overview_em_locking.html
http://www.objectdb.com/java/jpa/persistence/lock
相关推荐
此实例讲解了如何使用ODBC(Open Database Connectivity)管理器来创建、配置和删除数据源。ODBC是微软提供的一个数据库访问标准,允许应用程序与各种数据库系统交互。在这个实例中,我们学习了如何使用C++代码来...
本资源名为“数据库经典实例讲解”,显然是一个包含了多种数据库应用实例的集合,旨在帮助学习者深入理解和掌握数据库的实际操作技巧。在这个压缩包中,尽管没有具体的文件内容展示,我们可以根据通常的数据库实例来...
《Visual C++ + SQL Server数据库应用系统开发与实例》是一本深入探讨如何使用Microsoft的C++编程语言结合SQL Server数据库进行应用系统开发的书籍。在提供的压缩包文件中,包含了Chp5、Chp6、Chp8和Chp7这四个章节...
在《Visual C++数据库应用实例完全解析》第四章中,我们深入探讨了如何利用Visual C++这一强大的开发工具与数据库进行交互,特别是结合SQL语言来实现数据管理与应用。本章内容涵盖了数据库基础、数据库连接、查询...
《Visual C++数据库编程技术与实例》是一本深入探讨如何使用Microsoft Visual C++进行数据库编程的专业书籍,由沈炜著。这本书旨在帮助开发者理解和掌握在C++环境下与数据库交互的各种技术和方法,尤其针对Visual ...
这个实例将展示如何使用数据库来存储图书元数据、读者记录和借阅历史,以及如何实现基于权限的访问控制,确保数据的安全性和准确性。 学院教学管理实例则关注教育机构的日常教务工作。这包括课程设置、教师信息、...
书中的实例可能会讲解如何在VC++中实现线程安全的数据访问,使用互斥锁、信号量等同步机制,确保数据的一致性和完整性。 六、界面设计与用户体验 VC++提供了丰富的MFC(Microsoft Foundation Classes)库用于构建...
《Visual C++数据库开发典型模块与实例精讲颜志军2》是面向VC++数据库开发者的实战指南,主要探讨如何利用Microsoft Visual C++这一强大的编程工具进行数据库应用系统的开发。本部分聚焦于深入讲解关键模块的实现和...
书中详细讲解了如何使用Visual C++进行数据库应用的开发,包括通用模块的设计与实现,以及一系列典型的系统开发实例。 在Visual C++编程中,数据库交互是一个关键部分。C++本身并不直接支持数据库操作,但通过引入...
1. 锁机制:行级锁、页级锁、表级锁等,确保多用户同时访问时的数据一致性。 2. 死锁:可能发生两个事务互相等待对方释放资源的情况,需要通过死锁检测和解除机制来避免。 通过以上讲解,你应该对数据库有了全面的...
《Delphi + SQL Server数据库应用实例完全解析》是一本深入探讨如何使用Delphi编程环境与Microsoft SQL Server数据库进行高效交互的书籍。这本书旨在为开发者提供详尽的指导,帮助他们理解并掌握在Delphi中设计、...
探讨SQL Server与其他应用系统的交互,如.NET Framework集成,以及使用ADO.NET进行数据库编程的方法。 11. **第16章:数据库安全及访问控制**: 安全是数据库管理的重要环节,本章将讲解权限管理、角色、登录账户...
在C++中,通常通过ODBC(开放数据库连接)或JDBC(Java数据库连接)等接口来使用SQL,这些接口提供了标准化的方式来访问各种不同的数据库管理系统。 ODBC是Microsoft提出的一个标准,它提供了一个中间层,使得C++...
《Visual C++数据库编程技术与实例》是一本深入探讨如何使用C++进行数据库应用程序开发的专业书籍。这本书的主要焦点是利用Microsoft的Visual C++环境来构建高效、稳定的数据库应用系统。通过学习,读者不仅可以掌握...
在设计数据库应用时,合理地管理事务能够确保数据在并发操作中的安全。 4. 安全性设计:数据库存储了企业大量的重要信息,因此安全性设计至关重要。设计时需要考虑用户权限的分配,以及对敏感数据的加密和访问控制...
8. **事务与并发控制**:数据库系统必须处理多个用户同时访问数据的情况。SQL Server 2005支持事务管理,确保数据一致性。了解事务的ACID属性(原子性、一致性、隔离性和持久性)和并发控制机制(如锁定和死锁)很...
《Visual C++ + SQL Server数据库应用系统开发与实例》是一本深入探讨如何使用Microsoft的C++编程语言结合SQL Server数据库进行应用系统开发的专业书籍。在提供的压缩包文件中,我们聚焦于"Chp9"这一章节,这通常...