statement 一次执行多条sql_MyBatis#基础执行器与一级缓存处理(二)

本文详细介绍了MyBatis的Executor执行体系,特别是Reuse Executor如何重用Statement以及一级缓存的工作原理。讨论了在批处理执行器BatchExecutor中,为何不能像重用执行器那样简单地重用Statement,同时分析了多线程环境下批处理可能遇到的问题。还深入探讨了一级缓存的命中条件,包括sql、参数、statementID、SqlSession的一致性,以及各种操作和配置对缓存的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先回顾一下(一)的内容

重用执行器 Reuse Executor

Q:一个SqlSession执行了selectList和selectOne,两个的sql语句都相同,是否可以命中Statement的缓存?

fd45a6670ecb00ed837499a3424596f2.png

A:可以命中缓存

所命中的缓存是JDBC中的ReuseStatement的PrepareStatement的缓存,不是一级缓存,只要最终的SQL语句一样就会命中Statement

UserMapper:

@CacheNamespace
public interface UserMapper {

    //两个SQL一样
    @Select({" select * from users where id=#{1}"})
    User selectByid(Integer id);


    @Select({" select * from users where id=#{1}"})
    User selectByid3(Integer id);
}

    @Test
    public void sessionByReuseTest(){
        SqlSession sqlSession = factory.openSession(ExecutorType.REUSE, true);
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        //两个select对应两个不同的 MappedStatement 映射,会不会采用一个预处理器Statement?
        //(JDBC的Statement)
        mapper.selectByid(10);
        mapper.selectByid3(10);

    }
题目所说的Statement指的是JDBC的Statement

bc0c92094ce984f60bc83e1de7482836.png

可以看到只引入了一次预编译

如果说两个SQL不一样

@CacheNamespace
public interface UserMapper {

    //两个SQL一样
    @Select({" select * from users where id=#{1}"})
    User selectByid(Integer id);


    @Select({" select * from users where id=#{1} and 1=1"})
    User selectByid3(Integer id);
}

    
    
//重用执行器
@Test
public void sessionByReuseTest(){
    SqlSession sqlSession = factory.openSession(ExecutorType.REUSE, true);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    //两个select对应两个不同的 MapperStatement,不会会采用一个预处理器Statement?
    mapper.selectByid(10);
    mapper.selectByid3(10);
}

e872572b0cdebcbc2cbf3327ce375411.png

可以看到引入了两次预编译

结论:只要是SQL语句相同,就会重复利用Statement,不论是doQuery还是doUpdate最终都还是会重复的使用Statement

查看源代码可以看到:

bd6050e5a872205dd864deae22ff1495.png
String是他的SQL语句
Statement就是他缓存的JDBC的Statement

查看doQuery源码

    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        //预编译SQL语句
        Statement stmt = this.prepareStatement(handler, ms.getStatementLog());
        return handler.query(stmt, resultHandler);
    }

在预编译过程prepareStatement中(对应jdbc中的prepareStatement)

b7c1e4ea82ea126ac8165b40c5dbfe5f.png
如果没有就通过StatementHandler构建一个新的Statement,之后put进缓存中

总结:在会话期间内,所有相同的SQL语句会采用同样的Statement,只有会话结束之后Statement才会一起关闭

批处理执行器

bcc4ae07b0967f98bab72a8ab626a507.png
会话把所有的请求都发往Batch Executor,而Batch Executor会把所有的SQL语句保存起来到Statement集合中(缓存),等到flushStatement时一起进行处理(所有的增删改同时进行处理,不管StatementID是否一样),但是不一定都采用同一个Statement
@Test
public void sessionBatchTest(){
    SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    mapper.setName(10,"道友友益永存");
    mapper.addUser(Mock.newUser());

    sqlSession.flushStatements();
}

Q:是否会使用同一个JDBC Statement?

A:不是同一个Statement

Q:set和add操作是一起提交的么?

A:是一起提交的

验证:此时断点查看

7097c3cfc0d35ebaf808eb37e6e6013f.png

此时数据还没有提交

593bf32b7a546ec24bd9c98dd983678a.png

继续执行后查看数据库

be067e0f5659d50a3cd3d8d430a4f64d.png
在我们调用setName和addUser时,在MyBatis层面不会真正的执行,而是把他们转换为对应的JDBC Statement并保存在暂存区中,最后再一个个的执行其中的语句
@Test
public void sessionBatchTest1(){
    SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    mapper.setName(10,"道友友益永存");
    mapper.addUser(Mock.newUser());
    mapper.addUser(Mock.newUser());
    mapper.addUser(Mock.newUser());
    mapper.setName(12,"道友友益永存");

    //执行后batchResults有几个结果?
    // A:2个 B:5个 C:3个
    sqlSession.flushStatements();
}

Q:执行后batchResults有几个结果?

A:3个

f48d0a23e6fdf72e5596f74262aa2872.png

Q:为什么是3个结果?什么情况下会采用同一个Statement?

1.sql 相同
2.MappedStatement相同
3.必须是连续的
在三个条件都满足的情况下才会使用同一个 JDBC statement

mapper.setName(10,"道友友谊永存");和mapper.setName(12,"道友友益永存");两者满足1、2条件不满足3条件,所以用了两个JDBC Statement

mapper.addUser(user);1、2、3条件都满足使用一个JDBC的Statement

所以最后共使用了3个JDBC的Statement

91905645587d20bfb188e17a40d989f3.png

如果按照这样的顺序,则结果为四个

@Test
public void sessionBatchTest1(){
    SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    mapper.setName(10,"道友友益永存");
    mapper.addUser(Mock.newUser());
    mapper.addUser(Mock.newUser());
    mapper.setName(12,"道友友益永存");
    mapper.addUser(Mock.newUser());

    List<BatchResult> batchResults = sqlSession.flushStatements();
}

2b59b6a76d7104526e26bb39909e01db.png

5f7290d488713bfd15a388f5efba024b.png

Q:为什么他不能像重用执行器一样,只要是相同的SQL语句就重用Statement从而减少编译?

A:如果按照重用执行器来执行如下代码

// 1.添加
// 2.执行
// 3.返回结果
@Test
public void sessionBatchTest(){
    SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    //MappedStatement
    // 1.sql 相同 2.MappedStatement 3.必须是连续的  =》JDBC statement
    mapper.setName(10,"道友友谊永存"); //添加 statement 把参数和SQL设置好
    User user = Mock.newUser();
    mapper.addUser(user);
    mapper.setName(user.getId(),"小鲁班");

    // JDBC statement?
    // 一起提交的吗?
    List<BatchResult> batchResults = sqlSession.flushStatements();
     // A:2个 B:5个 C:3个

}

那么两个mapper.setName就会合并,但是mapper.addUser(user);还没有执行,也就是说第二个mapper.setName中的user.getId()为null,最后执行出错

每次执行JDBC操作时,都会添加 statement 把参数和SQL设置好,保存在Statement中。如果是相邻的那么自然就会重用Statement,再调用addBatch完成批处理的添加操作

下面进行断点查看原理

1.添加

caefbcbb167a7f5a80f0e00e76366bf9.png

mapper.setName(10,"道友友谊永存");,在doupdate()中进行 第一次添加

9df97b8269ffbf884c0a0bbcaf35fae2.png

执行mapper.addUser(user);,在doupdate()中进行 第二次添加

fda6f32f9fc2e292867de2af3b419651.png

依然进行创建

7fd0b3e1046f96b44bae67cc55e1ed0e.png

执行mapper.setName(user.getId(),"小鲁班");

95e259b15617502279d5f8db43bf813f.png

同样是添加操作

73aead1806d14f6074d94e43b840cff6.png

所以最后我们可以看到在StatementList中有3个元素

43d9aeb2aafb1a34248888f89768d98f.png

在batchResultList中也有3个元素

c19703053786f8be4128930a3524ca88.png

此时我们的Statement还没有真正执行修改操作

在执行sqlSession.flushStatements();进而调用doFlushStatements时才会真正执行修改操作

24d3046eb0f0b8a427c3dccd9667747a.png

Q:在多线程条件下去执行BatchExecutor是否会产生问题?-- 可以跟面试官聊聊

467d6b513c9ba8a1a62133fa5d7c7013.png

470cf61285811d9d97e49f5827327333.png

A:会产生问题,所以Executor不是线程安全的,不能进行跨线程的使用

注意:无论是否重用Statement,对于StatementHandler来说都是新的

6f0a8438bf8ee09e8aafda9981637c78.png

Executor执行体系总结

b5091898219abf0666989caefd4f4a93.png

4046db3c1e6ddd3b91d5a948d7a078db.png

一级缓存

429bce2ac1a455a47163d08754adb677.png

什么情况下会命中一级缓存?

(一级缓存默认打开)存储接口为key-value(HashMap)

a9f9d89d4c8af58aa88f93703788d371.png

与两个参数有关

  1. 运行时参数相关
  2. 操作与配置相关

运行时参数相关

(1)sql 和参数必须相同

总结

f955a7f66795533ada5cf194a5861110.png
public class FirstCacheTest {
    private SqlSessionFactory factory;
    private SqlSession sqlSession;

    @Before
    public void init() throws SQLException {
        // 获取构建器
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        // 解析XML 并构造会话工厂
        factory = factoryBuilder.build(ExecutorTest.class.getResourceAsStream("/mybatis-config.xml"));
        sqlSession = factory.openSession();
    }
    @Test
    public void test3(){
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        User user = mapper.selectByid(10);
        User user1 =mapper.selectByid(10);
        System.out.println(user == user1);
    }
}

命中缓存

51cf721876d9a84b885cc9077930f63f.png

(2)必须是相同的statementID

如果SQL相同,参数不同则不会命中 一级缓存

@Test
public void test3(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user = mapper.selectByid(10);
    User user1 =mapper.selectByid(12);
    System.out.println(user == user1);
}

f8a873d7736498f17516574b45d2c1f7.png

如果 sql 和参数都相同,但方法名不相同同样无法命中一级缓存

@Test
public void test3(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    //statemetID = org.coderead.mybatis.UserMapper.selectByid
    User user = mapper.selectByid(10);
    //statemetID = org.coderead.mybatis.UserMapper.selectByid3
    User user1 =mapper.selectByid3(10);
    System.out.println(user == user1);
}

2704031a9ba67d96cdd7ba6414286144.png

(3)sqlSession必须一样 (会话级缓存)

如果 sql 和参数相同,statementID也相同,但是SqlSession不同,也不会命中一级缓存

@Test
public void test3(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectByid(10);
    
    User user1 = factory.openSession().getMapper(UserMapper.class).selectByid(10);
    System.out.println(user == user1);
}

e2b8213f7739042d90f17bf29d9dfda5.png

(4)RowBounds 返回行范围必须相同

如果这样如下调用也可以命中缓存

@Test
public void test3(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectByid(10);

    Object user1 = sqlSession.selectOne("org.coderead.mybatis.UserMapper.selectByid", 10);
    System.out.println(user == user1);
}

81dc86738ddfdaa1b3762641a4713bf5.png

如果设置了分页,则不能命中一级缓存(RowBounds.DEFAULT时可以命中缓存)

@Test
public void test3(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectByid(10);

    //分页,返回0-10行
    RowBounds rowBounds = new RowBounds(0,10);

    List<Object> user1 = sqlSession.selectList(
            "org.coderead.mybatis.UserMapper.selectByid", 10, rowBounds);
    System.out.println(user == user1.get(0));
}

3ce88679d8eeb7b6953019a9350c0ba0.png

操作与配置相关

(1)未手清空才能命中缓存,否则不能命中缓存

@Test
public void test1(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectByid(10);
    sqlSession.clearCache();
    User user1 = mapper.selectByid(10);
    System.out.println(user == user1);
}

209675e85425b3e13d7508a0b745419f.png
注意,虽然编译是一次,但是查询查的是两次

(2)未调用 flushCache=true的查询才可能命中缓存

设置UserMapper中的selectByid的Options.FlushCachePolicy.TRUE,则不能命中一级缓存

@Select({"select * from users where id=#{1}"})
@Options(flushCache = Options.FlushCachePolicy.TRUE)
User selectByid(Integer id);

01e377055cdad1d27e027210ab2b65fe.png

若,此时Mapper中如下所示

    @Select({"select * from users where id=#{1}"})
    @Options(flushCache = Options.FlushCachePolicy.TRUE)
    User selectByid(Integer id);


    @Select({"select * from users where id=#{1}"})
    User selectByid3(Integer id);

则在执行mapper.selectByid(10);后同样会清空一级缓存

因为Options.FlushCachePolicy.TRUE = sqlSession.clearCache();

@Test
public void test1(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user = mapper.selectByid3(10);
    mapper.selectByid(10);
    User user1 = mapper.selectByid3(10);

    System.out.println(user == user1);
}

5e430ad88f71c5ca5b58cb9f6f40b74d.png

(3)未执行Update 才可能命中一级缓存

public void test1(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    
    User user = mapper.selectByid3(10);
    mapper.setName(10, "道友永存");//update

    //数据一致性问题,相当于调用了 sqlSession.clearCache();
    User user1 = mapper.selectByid3(10);

    System.out.println(user == user1);
}

f532116c1369ffed982543b5849fbe9a.png

(4)缓存作用域不是 STATEMENT,STATEMENT表示关闭一级缓存(只是将作用域缩小至Statement,并不是真正关闭,若子查询引用了父类ID则还是可以命中一级缓存)

<settings>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

总结:

f955a7f66795533ada5cf194a5861110.png
sqlSession.commit和sqlSession.rollback一样会去清空一级缓存,相当于clearCache()

一级缓存源码解析

ea9aae700b66af94836e686eb77373b5.png

运行时参数相关

测试一级缓存代码:

@Test
public void test3(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectByid(10);
    User user1 = mapper.selectByid(10);
    System.out.println(user == user1);
}

11a4cc62b1ba8a62399d1b0875ad0436.png

1093d197332422b4a46bdff5f65047d9.png
queryStack表示嵌套查询

在一级缓存中是以HashMap来存储的

ffa8957af816967d9c9ca3a0214699e5.png

查看CacheKey中保存的数据

633b629f8a4c02c55f87ff85585120d4.png

87143aa2f30c7a608e514282bfdc6cc6.png

第一次执行select,返回是null,去数据库查询queryFromDatabase

e9f6e28afecbf2b52aa4c310bf0bc0aa.png

查询完成之后将结果保存至一级缓存

698c9fc11ab0f11cea9bd894854986be.png

dbdf9fb308ebd95565eafd452d0f4370.png

接着走第二次查询

bb9fb48ecebd46c33b405b4456f92abe.png

此时可以在一级缓存中找到所缓存的数据,最后直接返回即可

64fe3ddbf56e2e525b96c9bd96cad028.png

222e1103e96c354679bb7bb106ca1628.png

操作与配置相关

只要执行了update就会清空一级缓存

70df336f3314c640adc27cc06ce9db95.png

如果配置了flushCache也会清空一级缓存(没有涉及到子查询)

5563955e79eebcd5e0ced4812b2120f4.png

query方法中,作用域如果是STATEMENT也会清空一级缓存

a1bd0c8667136fd6af8346987aeb5eae.png

手动提交或回滚时也会清空一级缓存

f4e34de61fee746c68f399b5e73cb8ec.png

09dcc74a141680a91403af82500092b4.png

总结:一级缓存和SqlSession相关,只要会话关闭,那么一级缓存全部清空

b27c871df1712f96090131bc0cceb41a.png

Spring继承 MyBatis 一级缓存失效违背了 与会话相关

如果集成时,没有配置事务的情况下每次都会构造新的会话

@Test
public void testBySpring(){
    ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring.xml");
    UserMapper mapper = context.getBean(UserMapper.class);

    User user = mapper.selectByid(10); 
    User user1 =mapper.selectByid(10);
    System.out.println(user == user1);
}

f705ab4ee748755ee0ef60ce9d09a060.png

因为在mapper.selectByid(10);时,每次都会构造一个新的会话 发起调用,即代码中是两个不同的会话

验证:因为Executor和缓存是一比一的关系,所以如果两个select是相同的Executor,那么就表示是同一个SqlSession,也就是同一个缓存

aa9df62db5deea6463a25e693fe1f1e1.png

64c65595f250e29df54ee9ce9d2a7115.png

可以发现是两个不同的Executor,不是同一级会话,所以不能命中缓存

只要加上事务,将两个查询放在一个事务中就可以命中一级缓存

@Test
public void testBySpring(){
    ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring.xml");
    UserMapper mapper = context.getBean(UserMapper.class);
    //   动态代理                         动态代理                  MyBatis
    // mapper ->SqlSessionTemplate --> SqlSessionInterceptor-->SqlSessionFactory

    DataSourceTransactionManager transactionManager =
            (DataSourceTransactionManager) context.getBean("txManager");
    // 手动开启事物
    TransactionStatus status = transactionManager
            .getTransaction(new DefaultTransactionDefinition());

    User user = mapper.selectByid(10); // 每次都会构造一个新会话 发起调用

    User user1 =mapper.selectByid(10);// 每次都会构造一个新会话 发起调用

    System.out.println(user == user1);
}

8bd8dc3ae56582a59e69452dc9ce5f38.png

对于Spring的context.getBean(UserMapper.class);是不同于MyBatis的Mapper的,Spring采用动态代理来执行

64a16294c6ab83cb3375983040525ab8.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值