• 多线程怎么共用一个事务


    场景

    有一天邱大神问我

    业务很简单,比如:我新增一个user使用事务,然后再这个事务里面创建了个线程,新增另外一个表的数据
    我要在第二个事务里面查询到第一个事务里面的user新增的数据

    分析

    如果在同一个事务里面的话,那么先提交了一个用户,后面的线程查询就能查询得到.
    那只需要这个方法使用的都是同一个SqlSession即可;

    测试

    创建个SqlContext获取SqlSession:

    
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    
    @Component
    public class SqlContext {
        @Resource
        private SqlSessionTemplate sqlSessionTemplate;
    
        public SqlSession getSqlSession(){
            SqlSessionFactory sqlSessionFactory = sqlSessionTemplate.getSqlSessionFactory();
            return sqlSessionFactory.openSession();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    好比我有个config表就建的key,value

    CREATE TABLE `config` (
      `id` int NOT NULL AUTO_INCREMENT,
      `config_key` varchar(20) NOT NULL,
      `config_value` varchar(500) NOT NULL,
      `modified_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `remark` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    两个测试方法,

    • 一个是testSession,通过sqlContext获取SqlSession,通过sqlSession获取ConfigDao,然后多线程方法内部也使用这个ConfigDao,然后手动提交事务回滚事务.然后分别先进行修改数据,然后在新增一条数据,另外一条线程内部进行查询数据,并且查询最新的一条数据.
    • 一个是testTransaction,通过spring的声明式事务,也是先进行修改数据,然后在新增一条数据,另外一条线程内部进行查询数据,并且查询最新的一条数据.

    对应的其他类我并没有贴出来,因为大家可以自己找个项目走一波

    测试testSession

     @Override
        @SneakyThrows
        public Config testSession(Long id) {
            // 获取数据库连接,获取sqlSession
            SqlSession sqlSession = sqlContext.getSqlSession();
            Connection connection = sqlSession.getConnection();
            try {
                // 设置手动提交
                connection.setAutoCommit(false);
                ConfigDao configDao = sqlSession.getMapper(ConfigDao.class);
                Config config = configDao.selectById(id);
                String testSession = "testSession:" + RandomUtil.randomString(3);
                config.setConfigValue(testSession);
                log.info("修改的value为:{}", testSession);
                configDao.updateById(config);
    
                Config insertConfig = new Config();
                insertConfig.setConfigKey(new DateTime().toString("HH:mm:ss"));
                insertConfig.setConfigValue(new DateTime().toString("HH:mm:ss"));
                configDao.insert(insertConfig);
                log.info("新增的id:" + insertConfig.getId());
    
                //另外一条线程执行
                CompletableFuture<Void> future = CompletableFuture.runAsync(
                        () -> {
                            //让最后一个线程抛出异常
                            Config config1 = configDao.selectById(id);
                            log.info("内部查询的value:{}", config1.getConfigValue());
    
                            QueryWrapper<Config> wrapper = new QueryWrapper<>();
                            wrapper.orderByDesc("id").last("limit 1");
                            Config config2 = configDao.selectOne(wrapper);
                            log.info("查询最新的id:" + config2.getId());
                        }
                );
                future.get();
                connection.commit();
                log.info("修改完毕");
                return config;
            } catch (Exception e) {
                connection.rollback();
                log.info("error", e);
                throw e;
            } finally {
                connection.close();
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    返回结果:跟我们预想的一样,相当于使用的同一个事务,可以看看打印的线程名称不同,一个是main,一个是onPool-worker-9;

    2023-09-07 15:15:02.238  INFO 2448 [           main] c.s.s.service.impl.ConfigServiceImpl     [52] : 修改的value为:testSession:owg
    2023-09-07 15:15:02.354  INFO 2448 [           main] c.s.s.service.impl.ConfigServiceImpl     [59] : 新增的id:10
    2023-09-07 15:15:02.365  INFO 2448 [onPool-worker-9] c.s.s.service.impl.ConfigServiceImpl     [66] : 内部查询的value:testSession:owg
    2023-09-07 15:15:02.399  INFO 2448 [onPool-worker-9] c.s.s.service.impl.ConfigServiceImpl     [71] : 查询最新的id:10
    2023-09-07 15:15:02.407  INFO 2448 [           main] c.s.s.service.impl.ConfigServiceImpl     [76] : 修改完毕
    
    • 1
    • 2
    • 3
    • 4
    • 5

    测试testTransaction

        @Override
        @Transactional
        @SneakyThrows
        public Config testTransaction(Long id) {
            Config config = baseMapper.selectById(id);
            String testSession = "testSession:" + RandomUtil.randomString(3);
            config.setConfigValue(testSession);
            log.info("修改的value为:{}", testSession);
            baseMapper.updateById(config);
    
            Config insertConfig = new Config();
            insertConfig.setConfigKey(new DateTime().toString("HH:mm:ss"));
            insertConfig.setConfigValue(new DateTime().toString("HH:mm:ss"));
            baseMapper.insert(insertConfig);
            log.info("新增的id:" + insertConfig.getId());
    
            //另外一条线程执行
            CompletableFuture<Void> future = CompletableFuture.runAsync(
                    () -> {
                        //让最后一个线程抛出异常
                        Config config1 = baseMapper.selectById(id);
                        log.info("内部查询的value:{}", config1.getConfigValue());
    
                        QueryWrapper<Config> wrapper = new QueryWrapper<>();
                        wrapper.orderByDesc("id").last("limit 1");
                        Config config2 = baseMapper.selectOne(wrapper);
                        log.info("查询最新的id:" + config2.getId());
                    }
            );
            future.get();
            log.info("修改完毕");
            return config;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    返回结果:

    2023-09-07 15:18:44.025  INFO 29104 [           main] c.s.s.service.impl.ConfigServiceImpl     [94] : 修改的value为:testSession:pl8
    2023-09-07 15:18:44.171  INFO 29104 [           main] c.s.s.service.impl.ConfigServiceImpl     [101] : 新增的id:11
    2023-09-07 15:18:44.189  INFO 29104 [onPool-worker-9] c.s.s.service.impl.ConfigServiceImpl     [108] : 内部查询的value:testSession:owg
    2023-09-07 15:18:44.242  INFO 29104 [onPool-worker-9] c.s.s.service.impl.ConfigServiceImpl     [113] : 查询最新的id:10
    2023-09-07 15:18:44.243  INFO 29104 [           main] c.s.s.service.impl.ConfigServiceImpl     [117] : 修改完毕
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意使用同一个sqlsession会导致线程安全问题,testSession方法就是在另外线程里面能读取到数据库里面没有的数据.但是有时候业务就是这么奇怪.

    扩展总结

    可以看看我的mybatis的简单解析:
    mybatis的源码解析:https://blog.csdn.net/qq_38366063/category_8574377.html

    为什么DefaultSqlSession线程不安全?
    首先由于JDBC的Connection对象本身不是线程安全的,而session中又只有一个connection,所以不是线程安全的
    一次SqlSession的执行最终只会产生一个connection,所以我们设想一下,在两个线程通过同一个sqlsession来执行crud,那么就有可能,我先跑完的线程,把唯一的这一个连接给关闭掉,从而造成另一条线程的逻辑不被成功执行,所以在方法里面通过创建SqlSession来执行数据库操作是线程不安全的。就会导致testSession方法的现象.

    为什么使用的mapper就不会出现线程安全问题?
    因为注入到service类里面的mapper是MapperProxy的代理类,内部是SqlSessionTemplate,而SqlSessionTemplate是线程安全的,因为每次执行方法都会走SqlSessionInterceptor拦截器,创建一个新的SqlSession(其实是从当前事务之外得到一个SqlSession,如果没有就创造一个新的。然后,如果事务被打开,且事务管理器是SpringManagedTransactionFactory时,将得到的SqlSession同当前事务同步,也就是说开启了事务,那么SqlSession就是当前事务内的那个SqlSession,所有开启了事务仍有一级缓存,不开启事务那么每次都新建一个SqlSession,那么此时一级缓存就会失效)

  • 相关阅读:
    C#基础详解(上)
    MyBatis的高级映射
    关于HashCode的问题
    【后端】Ubuntu开放mysql端口访问;如何开放服务器mysql给其他ip使用;在Ubuntu/Linux环境下开放3306端口
    40. 组合总和 II
    mac电脑监控软件哪个好
    Bootstrap的卡片组件相关知识
    【解决】openeuler22部署k8s提示/opt/cni/bin缺少资源问题
    解决Linux磁盘已满
    数字信号处理——CFAR检测器设计(2)
  • 原文地址:https://blog.csdn.net/qq_38366063/article/details/133846250