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

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

时间: 2024-07-30 20:30:43

MyBatis一级缓存引起的无穷递归的相关文章

MyBatis一级缓存、二级缓存

一.MyBatis一级缓存 MyBatis默认启动一级缓存,一级缓存是SqlSession级别的 注意:有两个因素会使一级缓存失效:  1.对SqlSession进行commit()操作(即对数据库进行了增.删.改操作).数据库中的数据发生了改变,此时若再从内存中读取缓存的数据,则会读取到错误的数据信息,所以此时旧的一级缓存中的数据会清空,当用户下一次执行查询操作时, 会重新从数据库中读取数据并放入一级缓存中  2.关闭SqlSession.一级缓存的设计是每个sqlsession单独使用一个缓

MyBatis 一级缓存与二级缓存

MyBatis一级缓存 MyBatis一级缓存默认开启,一级缓存为Session级别的缓存,在执行以下操作时一级缓存会清空 1.执行session.clearCache(); 2.执行CUD操作 3.session.close(); //不是同一个Session对象了 MyBatis二级缓存 需要配置<cache></cache> 是一个映射文件级的缓存 使用Mybatis二级缓存时查询的对象实体类必须序列化实现(实现Serializable接口) 二级缓存使用时 必须使用sess

Mybatis一级缓存和二级缓存总结

1:mybatis一级缓存:级别是session级别的,如果是同一个线程,同一个session,同一个查询条件,则只会查询数据库一次 2:mybatis二级缓存:级别是sessionfactory级别的,是针对于各个线程发出的sql查询条件 3:spring 关闭了mybatis的一级缓存,每一次查询都会建立一次连接,创建新的session,源码类有: MapperFactoryBean.SqlSessionDaoSupport.SqlSessionTemplate:在SqlSessionTem

MyBatis系列目录--5. MyBatis一级缓存和二级缓存(redis实现)

转载请注明出处哈:http://carlosfu.iteye.com/blog/2238662 0. 相关知识: 查询缓存:绝大数系统主要是读多写少. 缓存作用:减轻数据库压力,提供访问速度. 1. 一级缓存测试用例 (1) 默认开启,不需要有什么配置 (2) 示意图 (3) 测试代码 Java代码   package com.sohu.tv.cache; import org.apache.ibatis.session.SqlSession; import org.junit.After; i

spring整合mybatis后,mybatis一级缓存失效的原因

这些天由于项目存在数据访问的性能问题,研究了下缓存在各个阶段的应用,一般来说,可以在5个方面进行缓存的设计: 1.最底层可以配置的是mysql自带的query cache, 2.mybatis的一级缓存,默认情况下都处于开启状态,只能使用自带的PerpetualCache,无法配置第三方缓存 3.mybatis的二级缓存,可以配置开关状态,默认使用自带的PerpetualCache,但功能比较弱,能够配置第三方缓存, 4.service层的缓存配置,结合spring,可以灵活进行选择 5.针对实

MyBatis 一级缓存和二级缓存及ehcache整合

一级缓存 什么是缓存?? 缓存是存储在内存(cache)中的数据,一般情况都存在内存,在内存数据存储满了,会存储到硬盘上(disk),或是在我们进行一些操作和配置也可以把缓存存储到磁盘中. 缓存的作用是什么?? 缓存的作用可以减轻数据库的压力,减少用户对数据库的访问,可以说用户对数据库进行的重复操作在缓存中就可以实现操作,提高用户体验. 下面这张图是缓存的理解图 曾删改会对缓存造成影响. 写个测试,测试一下缓存是否存在:   答案是肯定的 现在测试一下进行曾删改数据,是否会对缓存造成影响? 二级

mybatis一级缓存二级缓存

Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言.所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库. 为什么要使用一级缓存,不用多说也知道

mybatis一级缓存和二级缓存(三)

缓存详细介绍,结果集展示 https://blog.csdn.net/u013036274/article/details/55815104 配置信息 http://www.pianshen.com/article/16399265/ ************详细介绍************* https://my.oschina.net/zjllovecode/blog/1817577?from=timeline&isappinstalled=0 一级缓存基于sqlSession默认开启,在操

mybatis——一级缓存、二级缓存

一.Mybatis缓存 ● MyBatis包含一个非常强大的查询緩存特性,它可以非常方便地定制和配置缓存.绶存可以极大的提升查询效率. ● MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存 ○ 默认情况下,只有一级缓存开启.( SqlSession级别的缓存,也称为本地缓存) ○ 二级缓存需要手动开启和配置,他是基于namespace级别的缓存. ○ 为了提高扩展性, MyBatis定义了缓存接口 Cache.我们可以通过实现 Cache接口来自定义二级缓存 小结:缓存的作用就是提升查