`
hzy0769
  • 浏览: 46125 次
  • 性别: 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

 

 

 

分享到:
评论

相关推荐

    Visual C++数据库实用编程100例_实例61-实例70

    此实例讲解了如何使用ODBC(Open Database Connectivity)管理器来创建、配置和删除数据源。ODBC是微软提供的一个数据库访问标准,允许应用程序与各种数据库系统交互。在这个实例中,我们学习了如何使用C++代码来...

    数据库经典实例讲解需要的朋友快看看吧

    本资源名为“数据库经典实例讲解”,显然是一个包含了多种数据库应用实例的集合,旨在帮助学习者深入理解和掌握数据库的实际操作技巧。在这个压缩包中,尽管没有具体的文件内容展示,我们可以根据通常的数据库实例来...

    VCSQL数据库应用系统开发与实例1

    《Visual C++ + SQL Server数据库应用系统开发与实例》是一本深入探讨如何使用Microsoft的C++编程语言结合SQL Server数据库进行应用系统开发的书籍。在提供的压缩包文件中,包含了Chp5、Chp6、Chp8和Chp7这四个章节...

    Visual C++数据库应用实例完全解析第四章

    在《Visual C++数据库应用实例完全解析》第四章中,我们深入探讨了如何利用Visual C++这一强大的开发工具与数据库进行交互,特别是结合SQL语言来实现数据管理与应用。本章内容涵盖了数据库基础、数据库连接、查询...

    VisualC++数据库编程技术与实例沈炜著3

    《Visual C++数据库编程技术与实例》是一本深入探讨如何使用Microsoft Visual C++进行数据库编程的专业书籍,由沈炜著。这本书旨在帮助开发者理解和掌握在C++环境下与数据库交互的各种技术和方法,尤其针对Visual ...

    数据库讲义,包含实例讲解

    这个实例将展示如何使用数据库来存储图书元数据、读者记录和借阅历史,以及如何实现基于权限的访问控制,确保数据的安全性和准确性。 学院教学管理实例则关注教育机构的日常教务工作。这包括课程设置、教师信息、...

    VC++数据库通用模块及典型系统开发实例导航

    书中的实例可能会讲解如何在VC++中实现线程安全的数据访问,使用互斥锁、信号量等同步机制,确保数据的一致性和完整性。 六、界面设计与用户体验 VC++提供了丰富的MFC(Microsoft Foundation Classes)库用于构建...

    Visual C++数据库开发典型模块与实例精讲颜志军2.rar

    《Visual C++数据库开发典型模块与实例精讲颜志军2》是面向VC++数据库开发者的实战指南,主要探讨如何利用Microsoft Visual C++这一强大的编程工具进行数据库应用系统的开发。本部分聚焦于深入讲解关键模块的实现和...

    Visual C++数据库通用模块及典型系统开发实例导航 pdf 和随书光盘源码

    书中详细讲解了如何使用Visual C++进行数据库应用的开发,包括通用模块的设计与实现,以及一系列典型的系统开发实例。 在Visual C++编程中,数据库交互是一个关键部分。C++本身并不直接支持数据库操作,但通过引入...

    详细讲解数据库的基本知识,让你入门,有很多简单的应用实例

    1. 锁机制:行级锁、页级锁、表级锁等,确保多用户同时访问时的数据一致性。 2. 死锁:可能发生两个事务互相等待对方释放资源的情况,需要通过死锁检测和解除机制来避免。 通过以上讲解,你应该对数据库有了全面的...

    Delphi_+SQL_Server数据库应用实例完全解析

    《Delphi + SQL Server数据库应用实例完全解析》是一本深入探讨如何使用Delphi编程环境与Microsoft SQL Server数据库进行高效交互的书籍。这本书旨在为开发者提供详尽的指导,帮助他们理解并掌握在Delphi中设计、...

    《大型数据库系统 管理、设计实例分析--基于SQL Server 》课件

    探讨SQL Server与其他应用系统的交互,如.NET Framework集成,以及使用ADO.NET进行数据库编程的方法。 11. **第16章:数据库安全及访问控制**: 安全是数据库管理的重要环节,本章将讲解权限管理、角色、登录账户...

    C++数据库编程技术与实例.

    在C++中,通常通过ODBC(开放数据库连接)或JDBC(Java数据库连接)等接口来使用SQL,这些接口提供了标准化的方式来访问各种不同的数据库管理系统。 ODBC是Microsoft提出的一个标准,它提供了一个中间层,使得C++...

    Visual C++数据库编程技术与实例 附带光盘代码

    《Visual C++数据库编程技术与实例》是一本深入探讨如何使用C++进行数据库应用程序开发的专业书籍。这本书的主要焦点是利用Microsoft的Visual C++环境来构建高效、稳定的数据库应用系统。通过学习,读者不仅可以掌握...

    8个数据库设计典型实例

    在设计数据库应用时,合理地管理事务能够确保数据在并发操作中的安全。 4. 安全性设计:数据库存储了企业大量的重要信息,因此安全性设计至关重要。设计时需要考虑用户权限的分配,以及对敏感数据的加密和访问控制...

    数据库开发与应用 sql server2005 课程实例的数据库

    8. **事务与并发控制**:数据库系统必须处理多个用户同时访问数据的情况。SQL Server 2005支持事务管理,确保数据一致性。了解事务的ACID属性(原子性、一致性、隔离性和持久性)和并发控制机制(如锁定和死锁)很...

    VC SQL数据库应用系统开发与实例3

    《Visual C++ + SQL Server数据库应用系统开发与实例》是一本深入探讨如何使用Microsoft的C++编程语言结合SQL Server数据库进行应用系统开发的专业书籍。在提供的压缩包文件中,我们聚焦于"Chp9"这一章节,这通常...

Global site tag (gtag.js) - Google Analytics