引言:

  最近在项目中参与了一个领取优惠劵的活动,当多个用户领取同一张优惠劵的时候,使用了数据库锁控制并发,起初的设想是:如果多个人同时领一张劵,第一个到达的人领取成功,其它的人继续查找是否还有剩余的劵,如果有,继续领取,否则领取失败。在实现中,我一开始使用了递归的方式去查找劵,实际的测试中发现出现了无穷递归,通过degug和查阅资料才发现这是由于mybatis的一级缓存引起的,以下将这次遇到的问题和大家分享讨论。

1.涉及到的知识点

Mybatis缓存:

一级缓存:默认开启,sqlSession级别缓存,当前会话中有效,执行sqlSession commit()、close()、clearCache()操作会清除缓存。[1]

二级缓存:需要手工开启,全局级别缓存,与mapper namespace相关。[1]

并发控制机制:

悲观锁:假定会发生并发冲突屏蔽一切可能违反数据完整性的操作。[2]

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[2] 乐观锁不能解决脏读的问题。

乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

2.代码

  以下是一个领取优惠劵的辅助方法-随机抽取一张优惠码,调用这个辅助方法的public方法开启了事务(开启了sqlSession)。实际测试的过程中发现,当数据库中只有一张优惠劵并且同时被多个用户领取时,会出现无穷递归。代码如下:

 1 /**
2 * 随机抽取一张优惠码
3 *
4 * @param codePrefix
5 * 优惠码前缀
6 * @return 优惠码 9 */
10 private String randExtractOneTicketCode(String mobile, String codePrefix) {
11 List<String> notExchangeCodeList = yzTicketCodeDaoExt.getTicketCodeList(codePrefix,
12 MobileServiceConstants.TICKET_CODE_STATUS_NOT_EXCHANGE);
13 logger.info("领取优惠劵>>>优惠劵可用数量{}",CollectionUtils.size(notExchangeCodeList));
14 if (CollectionUtils.isEmpty(notExchangeCodeList)) {
15 logger.warn("领取优惠劵>>>优惠劵{}已领完", codePrefix);
16 throw new YzRuntimeException(MobileServiceConstants.TICKET_NOT_REMAINDER);
17 }
18
19 int randomIndex = random.nextInt(notExchangeCodeList.size()); // 随机的索引
20 String ticketCode = notExchangeCodeList.get(randomIndex); // 随机选择的优惠码
21 YzTicketCode ticketCodeObj = yzTicketCodeDaoExt.getByCode(ticketCode);
22 if (ticketCodeObj == null
23 || ticketCodeObj.getStatus() != MobileServiceConstants.TICKET_CODE_STATUS_NOT_EXCHANGE) {
24 // 如果优惠劵已被使用
25 logger.info("领取优惠劵>>>优惠劵码{}不存在或已被使用",ticketCode);
26 return randExtractOneTicketCode(String mobile, String codePrefix);  //递归查找
27 }
28 /*
29 * 更新优惠码状态
30 */
31 ticketCodeObj.setExchangeTime(Calendar.getInstance().getTime());
32 ticketCodeObj.setStatus(MobileServiceConstants.TICKET_CODE_STATUS_HAD_EXCHANGED);
33 ticketCodeObj.setMobile(mobile);
34 int updateCnt = yzTicketCodeDaoExt.update4Receive(ticketCodeObj);
35 if(updateCnt <= 0){
36 //乐观锁,没有影响到行,表明更新失败,可能是该劵不存在或已被使用
37 logger.info("领取优惠劵>>>优惠劵码{}不存在或已被使用",ticketCode);
38 return randExtractOneTicketCode(String mobile, String codePrefix);  //递归查找,发现这里出现了循环递归
39 };
40 return ticketCode;
41 }

  通过debug发现,第38行出现了循环递归,原因是第11行执行的查询结果被mybatis一级缓存缓存了,导致每次查询的结果都是第一次查询的结果(有一张劵可以被领取),但实际上这张劵已经被其它用户领取了,从而发生了无穷递归。

 3.解决方案

1)编程式事务,通过transactionManager来获取sqlSession,然后通过sqlSession的clearCache()方法来清除一级缓存。

2)由于项目中使用了Spring申明式事务,并且并发量不高,考虑到减少复杂度,选择了简单的方法,直接提示用户系统繁忙。

/**
* 随机抽取一张优惠码
*
* @param codePrefix
* 优惠码前缀
* @return 优惠码
* @throws YzRuntimeException
* 如果没有可用的优惠劵
*/
private String randExtractOneTicketCode(String mobile, String codePrefix) {
List<String> notExchangeCodeList = yzTicketCodeDaoExt.getTicketCodeList(codePrefix,
MobileServiceConstants.TICKET_CODE_STATUS_NOT_EXCHANGE);
logger.info("领取优惠劵>>>优惠劵可用数量{}",CollectionUtils.size(notExchangeCodeList));
if (CollectionUtils.isEmpty(notExchangeCodeList)) {
logger.warn("领取优惠劵>>>优惠劵{}已领完", codePrefix);
throw new YzRuntimeException(MobileServiceConstants.TICKET_NOT_REMAINDER);
} int randomIndex = random.nextInt(notExchangeCodeList.size()); // 随机的索引
String ticketCode = notExchangeCodeList.get(randomIndex); // 随机选择的优惠码
YzTicketCode ticketCodeObj = yzTicketCodeDaoExt.getByCode(ticketCode);
if (ticketCodeObj == null
|| ticketCodeObj.getStatus() != MobileServiceConstants.TICKET_CODE_STATUS_NOT_EXCHANGE) {
// 如果优惠劵已被使用
logger.info("领取优惠劵>>>优惠劵码{}不存在或已被使用",ticketCode);
throw new YzRuntimeException(MobileServiceConstants.TICKET_SYSTEM_BUSY);
}
/*
* 更新优惠码状态
*/
ticketCodeObj.setExchangeTime(Calendar.getInstance().getTime());
ticketCodeObj.setStatus(MobileServiceConstants.TICKET_CODE_STATUS_HAD_EXCHANGED);
ticketCodeObj.setMobile(mobile);
int updateCnt = yzTicketCodeDaoExt.update4Receive(ticketCodeObj);
if(updateCnt <= 0){
//乐观锁,没有影响到行,表明更新失败,可能是该劵不存在或已被使用
logger.info("领取优惠劵>>>优惠劵码{}不存在或已被使用",ticketCode);
throw new YzRuntimeException(MobileServiceConstants.TICKET_SYSTEM_BUSY);
};
return ticketCode;
}

总结:

  现在项目大多使用集群的方式,使用java提供的并发机制已经无法控制并发,常用的是数据库锁和Redis提供的并发控制机制,上面代码中使用了数据库的乐观锁,乐观锁相比于悲剧锁而言,需要编写外部算法,错误的外部算法和异常恢复容易导致未知的错误,需要谨慎的设计和严格的测试。

参考文档:

[1]http://www.mamicode.com/info-detail-890951.html

[2]Concurrent Control http://en.wikipedia.org/wiki/Concurrency_control

最新文章

  1. .NET 基础 一步步 一幕幕[面向对象之方法、方法的重载、方法的重写、方法的递归]
  2. 如何dos命令打开服务窗口?
  3. GCD工作单元
  4. zw版_Halcon图像交换、数据格式、以及超级简单实用的DIY全内存计算.TXT
  5. hdu4642 Fliping game ——博弈
  6. 【转载/修改】ScrollLayout代码修正,追加模仿viewpager滚动速度
  7. 《我是一只IT小小鸟》阅读笔记
  8. (转)SQL Server2005 异常处理机制(Begin try Begin Catch)
  9. FAQ: Python中if __name__ == &#39;__main__&#39;:作用
  10. 解决Ubuntu和Windows该文件乱码问题
  11. docker- 构建 oracle2c-r2(12.2.0.1) 的镜像
  12. 学习总结------Servlet的简单理解
  13. 闲话和grunt
  14. hadoop 基本命令
  15. java桌面程序打包教程
  16. Koltin——最详细的可见性修饰符详解
  17. MongoDB学习第七篇 --- sql和mongodb对比
  18. #Java学习之路——基础阶段(第十一篇)
  19. web中绝对路径换虚拟路径
  20. [HDFS Manual] CH7 ViewFS Guide

热门文章

  1. Delphi 能不能从Ring 3进入Ring 0
  2. 查看内存数据的函数(ByteToHex和ByteToBin,最终都变成String)
  3. SSH反向连接让外网也可远程访问内网机器
  4. 成都大数据Hadoop与Spark技术培训班
  5. Xamarin 后台持续定位与提示
  6. Bootstrap,Foundation和TypeScript
  7. Swift - 自定义单元格实现微信聊天界面
  8. datarow用linq查询
  9. swt,jface,rcp
  10. linux 下opensplice的简易安装