`
hzy0769
  • 浏览: 45586 次
  • 性别: Icon_minigender_1
  • 来自: 东莞
社区版块
存档分类
最新评论

数据库并发访问时锁应用实例讲解

阅读更多

开始之前

       本文主要讲解各种常见锁策略的应用,希望通过这种实例讲解能让大家更清晰的理解各种锁的区别,在实际项目的该如何选择。由于本文的代码例子使用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("下单失败:库存不足");

}

 

如果updatesql返回0,说明数据已经被其他事务更新了,返回失败提示

重启后再次测试并发,变成以下这样

测试结果3(使用乐观锁)

Thread-1:查询最新库存剩余数量=1

Thread-2:查询最新库存剩余数量=1

Thread-1:判断库存数量1>=购买数量1,允许下单

Thread-2:判断库存数量1>=购买数量1,允许下单

Thread-1:更新库存数量=0version1,提交事务,下单成功

Thread-2:更新库存数量=0version1,提交事务,抛异常“数据过期”,回滚

 

现在相比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,这样下单不会成功,也不需要更新库存。

 

 

       如上图:当前测试商品的库存为0version=10

 

       然后进行一次下单操作看看,结果当然和预期一样返回下单失败,这时打开服务器的日志看(下图):

 

 

       系统自动对版本号进行了更新,在打开商品库存表看看:

 

 

       库存还是0没变,但版本号更新为11了。

 

 

方案三:使用悲观锁(Optimistic locking

当事务需要修改一个可能被其他事务同时修改的实体时,事务会发起一个命令将实体锁住。所有的锁会持续到事务结束后再自动释放。悲观锁通常使用数据库中的锁机制实现,数据库中的锁机制又分为共享锁(S)和排它锁(X),分别对应JPA中常用的两种悲观锁策略:PESSIMISTIC_READPESSIMISTIC_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返回的错误信息完全一样,个人理解是在mysqlSERIALIZABLE隔离级别实现相当于自动对所有查询获取共享锁,而我们使用锁策略是只对指定的查询获取锁。

另外由于我并不清楚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

 

 

 

分享到:
评论

相关推荐

    实例讲解MySQL中乐观锁和悲观锁

    悲观并发控制主要应用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本环境 悲观锁,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守...

    SQL Server 2008数据库设计与实现

    通过将理论融入数据库实践,清晰地讲解了关系型数据库的设计原则,完整地展示了如何进行良好的关系型数据库设计,深入揭示了SQL Server 2008的技术细节。  本书浓缩了作者作为SQL Server数据库架构师多年来丰富的...

    java concurrent source code

    结合大量实例,全面讲解Java多线程编程中的并发访问、线程间通信、锁等最难突破的核心技术与应用实践 封底 Java多线程无处不在,如服务器、数据库、应用。多线程可以有效提升计算和处理效率,大大提升吞吐量和可...

    java多线程核心技术

    结合大量实例,全面讲解Java多线程编程中的并发访问、线程间通信、锁等最难突破的核心技术与应用实践 Java多线程无处不在,如服务器、数据库、应用。多线程可以有效提升计算和处理效率,大大提升吞吐量和可伸缩性,...

    NHibernate 入门之旅(数据库教程)PDF版.rar

    因此本书围绕这些主要讲解NHibernate是什么、如何建立第一个NHibernate程序、NHibernate实例分析、NHibernate基本数据库操作:探索Insert, Update, Delete等、并发控制、初探延迟加载机制等内容。

    21天学会SQL

    第二篇讲解数据库管理的常用知识,包括数据库的管理、表的管理、确保数据的完整性及用户权限的设置等内容。第三篇主要讲解SQL的编程,包括T-SQL语言、存储过程及触发器。第四篇讲解与商业智能有关的内容,包括集成...

    精通SQL 结构化查询语言详解

    《精通SQ:结构化查询语言详解》全面讲解SQL语言,提供317个典型应用,读者可以随查随用,针对SQL Server和Oracle进行讲解,很有代表性。 全书共包括大小实例317个,突出了速学速查的特色。《精通SQ:结构化查询语言...

    深入解析OracleDBA入门进阶与诊断案例 3/4

     本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...

    深入解析OracleDBA入门进阶与诊断案例 2/4

     本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...

    深入解析OracleDBA入门进阶与诊断案例 4/4

     本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...

    Nhibernate从入门到精通

    因此本书围绕这些主要讲解NHibernate是什么、如何建立第一个NHibernate程序、NHibernate实例分析、NHibernate基本数据库操作:探索Insert, Update, Delete等、并发控制、初探延迟加载机制等内容。

    MyISAM InnoDB 区别

    InnoDB和MyISAM是许多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣,视具体应用而定。基本的差别为:MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持。MyISAM类型的表强调的是性能,其执行数度...

    深入解析Oracle.DBA入门进阶与诊断案例

     本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...

    asp.net知识库

    在.NET访问MySql数据库时的几点经验! 自动代码生成器 关于能自定义格式的、支持多语言的、支持多数据库的代码生成器的想法 发布Oracle存储过程包c#代码生成工具(CodeRobot) New Folder XCodeFactory3.0完全攻略--...

    最新Python3.5零基础+高级+完整项目(28周全)培训视频学习资料

    继承实例讲解 多态实例讲解 本节作业之选课系统开发 第7周 心灵分享 上节回顾 静态方法、类方法、属性方法 课堂扯淡 深入讲解类的特殊成员方法__init__等 深入讲解类的特殊成员方法__new__等 反射详解 异常处理Try...

    iBATIS实战

    书中既详实地介绍了iBATIS的设计理念和基础知识,也讨论了动态SQL、高速缓存、DAD框架等高级主题,还讲解了iBATIS在实际开发中的应用。书的最后给出了一个设计优雅、层次清晰的示例程序JGameStore,该示例涵盖全书的...

    python 高效开发实战源代码+pdf

    也许你听说过全栈工程师,...本书内容精练、重点突出、实例丰富、讲解通俗,是广大网络应用设计和开发人员不可多得的一本参考书,同时非常适合大中专院校师生学习和阅读,也可作为高等院校计算机及相关培训机构的教材。

Global site tag (gtag.js) - Google Analytics