zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    Spring IOC三种注入方式(接口注入、setter注入、构造器注入)(摘抄)
    java设计模式之代理模式
    JAVA学习中好网站
    程序员学习能力提升三要素
    ognl中的#、%和$
    二级缓存
    hibernate主键自动生成
    专科考研学校要求一览表
    Chapter 3: Connector(连接器)
    Chapter 2: A Simple Servlet Container
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/5377121.html
Copyright © 2011-2022 走看看