MyBatis一级缓存引起的无穷递归

  最近在项目中参与了一个领取优惠劵的活动,当多个用户领取同一张优惠劵的时候,使用了数据库锁控制并发,起初的设想是:如果多个人同时领一张劵,第一个到达的人领取成功,其它的人继续查找是否还有剩余的劵,如果有,继续领取,否则领取失败。在实现中,我一开始使用了递归的方式去查找劵,实际的测试中发现出现了无穷递归,通过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操作,上面代码中使用了数据库的乐观锁,乐观锁相比于悲剧锁而言,需要编写外部算法,错误的外部算法和异常恢复容易导致出现未知的错误,需要谨慎的设计和严格的测试。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值