MyBatis一级缓存引起的无穷递归
2016-04-08 09:04
204 查看
引言:
最近在项目中参与了一个领取优惠劵的活动,当多个用户领取同一张优惠劵的时候,使用了数据库锁控制并发,起初的设想是:如果多个人同时领一张劵,第一个到达的人领取成功,其它的人继续查找是否还有剩余的劵,如果有,继续领取,否则领取失败。在实现中,我一开始使用了递归的方式去查找劵,实际的测试中发现出现了无穷递归,通过degug和查阅资料才发现这是由于mybatis的一级缓存引起的,以下将这次遇到的问题和大家分享讨论。1.知识储备
简单介绍:
Mybatis
一级缓存:默认开启,sqlSession级别缓存,当前会话中有效,执行sqlSession commit()、close()、clearCache()操作后会清除缓存。
二级缓存:需要手工开启,全局级别缓存,与mapper namespace相关。
详情参见:http://www.mamicode.com/info-detail-890951.html
2.代码示例
以下是一个领取优惠劵的辅助方法-随机抽取一张优惠码,调用这个辅助方法的public方法开启了事务。实际测试的过程中发现,当数据库中只有一张优惠劵时并且同时被多个用户领取时,会出现无穷递归。代码如下:
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发现,第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操作,上面代码中使用了数据库的乐观锁,乐观锁相比于悲剧锁而言,需要编写外部算法,错误的外部算法和异常恢复容易导致出现未知的错误,需要谨慎的设计和严格的测试。
相关文章推荐
- python 小模块 ---marshai模块
- jQuery - AJAX 简介
- JavaScript代码实现左右上下自动晃动自动移动
- 【自定义标签开发】04-简单标签库功能详解
- ie8如何使用rgba样式
- MySQL绿色版 官网下载+安装(win7)
- Android初学者共勉
- 杭州--4.7
- 每天laravel-20160708|Repository
- UI界面设计---------计时器
- Linux学习--gdb调试
- java byte 总结
- 052(三十七)
- 程序员应该知道的10大基础算法
- 画图
- smartforms金额或者数量字段显示不出来
- Apache2.4.16+SVN1.7.9+svnmanager1.10 升级 (一)
- AD 快捷键
- 在word中调用CDR(coreldraw)图形,变形的问题解决方法介绍!
- ExtJS小技巧