MyBatis一级缓存引起的无穷递归
引言:
最近在项目中参与了一个领取优惠劵的活动,当多个用户领取同一张优惠劵的时候,使用了数据库锁控制并发,起初的设想是:如果多个人同时领一张劵,第一个到达的人领取成功,其它的人继续查找是否还有剩余的劵,如果有,继续领取,否则领取失败。在实现中,我一开始使用了递归的方式去查找劵,实际的测试中发现出现了无穷递归,通过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
MyBatis一级缓存引起的无穷递归的更多相关文章
- Mybatis一级缓存的锅
问题背景 项目开发中有一个树形数据结构,不像经典组织结构树.菜单级别树,我们这个树形结构是用户后期手动建立起来的关系.因此数据库表结构为两张表:数据记录表.记录关系表,通过业务规则限制,形成的树形结构 ...
- MyBatis 一级缓存与二级缓存
MyBatis一级缓存 MyBatis一级缓存默认开启,一级缓存为Session级别的缓存,在执行以下操作时一级缓存会清空 1.执行session.clearCache(); 2.执行CUD操作 3. ...
- mybatis一级缓存详解
mybatis缓存分为一级缓存,二级缓存和自定义缓存.本文重点讲解一级缓存 一:前言 在介绍缓存之前,先了解下mybatis的几个核心概念: * SqlSession:代表和数据库的一次会话,向用户提 ...
- 0065 MyBatis一级缓存与二级缓存
数据库中数据虽多,但访问频率却不同,有的数据1s内就会有多次访问,而有些数据几天都没人查询,这时候就可以将访问频率高的数据放到缓存中,就不用去数据库里取了,提高了效率还节约了数据库资源 MyBatis ...
- MyBatis 一级缓存避坑
MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选: package org.apache.ibatis.session; /** * @autho ...
- 关于mybatis 一级缓存引发的问题
场景: 由于在一个方法中存在多个不同业务操作 private void insertOrUpdateField(CompanyReport entity) { //计算并数据 calcReportDa ...
- MyBatis一级缓存(转载)
<深入理解mybatis原理> MyBatis的一级缓存实现详解 及使用注意事项 http://demo.netfoucs.com/luanlouis/article/details/41 ...
- MyBatis 一级缓存、二级缓存全详解(一)
目录 MyBatis 一级缓存.二级缓存全详解(一) 什么是缓存 什么是MyBatis中的缓存 MyBatis 中的一级缓存 初探一级缓存 探究一级缓存是如何失效的 一级缓存原理探究 还有其他要补充的 ...
- Mybatis一级缓存和二级缓存总结
1:mybatis一级缓存:级别是session级别的,如果是同一个线程,同一个session,同一个查询条件,则只会查询数据库一次 2:mybatis二级缓存:级别是sessionfactory级别 ...
随机推荐
- C#用正则表达式去掉Html中的script脚本和html标签
原文 C#用正则表达式去掉Html中的script脚本和html标签 /// <summary> /// 用正则表达式去掉Html中的script脚本和html标签 ...
- 一个与Log4j相关的死锁(转)
这个死锁的原因:一个动作需要两个临界对象.静态同步方法,就是一个临界对象.这种场景,静态同步方法每次只能有一个线程持有.如果存在另一个临界对象,静态同步方法中也需要获取这个临界对象.即一个动作需要两个 ...
- C++运算符重载的方法
运算符重载的方法是定义一个重载运算符的函数,在需要执行被重载的运算符时,系统就自动调用该函数,以实现相应的运算.也就是说,运算符重载是通过定义函数实现的. 运算符重载实质上是函数的重载 重载运算符的函 ...
- hdu 4778 Rabbit Kingdom(减少国家)
题目链接:hdu 4778 Rabbit Kingdom 题目大意:Alice和Bob玩游戏,有一个炉子.能够将S个同样颜色的宝石换成一个魔法石.如今有B个包,每一个包里有若干个宝石,给出宝石的颜色. ...
- Ajax - 异步处理(点击变成文本框并修改)
效果: 对应的文档结构: Test.aspx 前台代码: 引入JQuery(jquery-1.8.3.min.js). 引入自己所写的JS代码(UserJS.js). <html xmlns=& ...
- Thinkphp学习04
原文:Thinkphp学习04 一.ThinkPHP 3 的输出 (重点) a.通过 echo 等PHP原生的输出方式在页面中输出 b.通过display方法输出 想分配变量可以使用as ...
- javascript --- 事件托付
javascript 之 事件托付 长处:1.提高性能(仅仅须要对父级进行操作,子节点相同会拥有其相关属性和方法) 2.对于新加入的事件.也让其拥有父级事件的属性 <!doctype html& ...
- 指尖上的电商---(3)Solr全文搜索引擎的配置
接上篇,Solr的准备工作完毕后,本节主要介绍Solr的安装,事实上Solr不须要安装.直接下载就能够了 1.Solr配置 下载地址 :http://lucene.apache.org/so ...
- gradle学习系列之eclipse中简单构建android项目
看不到图片能够去訪问这个网址看看:http://pan.baidu.com/s/1o6FrFkA 一.什么是Gradle 官网www.gradle.org上介绍Gradle是升级版(evolved)的 ...
- ThinkPHP框架视图详细介绍 View 视图--模板(九)
原文:ThinkPHP框架视图详细介绍 View 视图--模板(九) 视图也是ThinkPHP使用的核心部分: 一.模板的使用 a.规则 模板文件夹下[TPL]/[分组文件夹/][模板主题文件夹/]和 ...