Spring boot+mybatis読み書き分離


spring boot + mybatis read/write splitting
参照先:https://blog.csdn.net/wuyongde_0922/article/details/70655185シナリオ4
全体的な考え方:AbstractRoutingDataSource+mybatis@Intercepts+DataSourceTransactionManager
主体の判断論理はいずれも上記の博文から来ており,ここではspringboot環境における構成方式のみを説明する.
バージョンの使用:
  • spring boot:2.0.2.RELEASE
  • mybatis.starter:1.3.1
  • druid:1.1.9
  • mysql:5.7

  • 1.druidを使用した2つのデータ・ソースの構成
    # application.yml(mybatis構成)
    spring:
      profiles:
        #       
        active: dev
    mybatis:
      configuration:
        #     
        map-underscore-to-camel-case: true
        #       
        default-fetch-size: 100
        # SQL       
        default-statement-timeout: 3000
        #         
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    

    #アプリケーション-dev.yml(データソース構成)
    write:
      datasource:
        username: root
        password: 123123
        url: jdbc:mysql://localhost:3306/db1?useSSL=false&useUnicode=true&characterEncoding=utf8
        #      
        maxActive: 1000
        #         
        initialSize: 100
        #            ,    
        maxWait: 60000
        #      
        minIdle: 500
        #              ,    
        timeBetweenEvictionRunsMillis: 60000
        #          ,,    
        minEvictableIdleTimeMillis: 300000
        validationQuery: select 1
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        #   PSCache,         PSCache   
        poolPreparedStatements: true
        maxOpenPreparedStatements: 20
        #          filters,       sql    ,'wall'     
        filters: stat, wall, slf4j
        #     DruidDataSource     
        useGlobalDataSourceStat: true
    
    read:
      datasource:
        username: root
        password: 123123
        url: jdbc:mysql://localhost:3306/db2?useSSL=false&useUnicode=true&characterEncoding=utf8
        #      
        maxActive: 1000
        #         
        initialSize: 100
        #            ,    
        maxWait: 60000
        #      
        minIdle: 500
        #              ,    
        timeBetweenEvictionRunsMillis: 60000
        #          ,,    
        minEvictableIdleTimeMillis: 300000
        validationQuery: select 1
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        #   PSCache,         PSCache   
        poolPreparedStatements: true
        maxOpenPreparedStatements: 20
        #          filters,       sql    ,'wall'     
        filters: stat, wall, slf4j
        #     DruidDataSource     
        useGlobalDataSourceStat: true
    

    コンピュータにmysqlが1つしかないため、テストを容易にするために、ここでは2つのデータベースシミュレーションを使用します.
    # DynamicDataSourceConfig
        @Bean(name = "writeDataSource")
        @Primary//      
        @ConfigurationProperties(prefix = "write.datasource")
        public DataSource writeDataSource() {
            return DataSourceBuilder.create().type(DruidDataSource.class).build();
        }
    
        @Bean(name = "readDataSource")
        @ConfigurationProperties(prefix = "read.datasource")
        public DataSource readDataSource() {
            return DataSourceBuilder.create().type(DruidDataSource.class).build();
        }
    

    2.直接データソースとしてAbstractRoutingDataSourceを構成する
    # DynamicDataSourceConfig
        @Bean(name = "dataSource")
        public DynamicDataSource getDynamicDataSource() {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            Map dataSourceMap = new HashMap<>();
            dataSourceMap.put(DynamicDataSourceGlobal.READ.name(), readDataSource());
            dataSourceMap.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource());
            //     map,AbstractRoutingDataSource  key      
            dynamicDataSource.setTargetDataSources(dataSourceMap);
            return dynamicDataSource;
        }
    

    #DynamicDataSource(AbstractRoutingDataSourceを継承し、割当てロジックを実現)
    public class DynamicDataSource extends AbstractRoutingDataSource {
        //         key
        @Override
        protected Object determineCurrentLookupKey() {
            DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();
            if(dynamicDataSourceGlobal == null
                    || dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE) {
                return DynamicDataSourceGlobal.WRITE.name();
            }
            return DynamicDataSourceGlobal.READ.name();
        }
    }
    

    # DynamicDataSourceGlobal
    public enum DynamicDataSourceGlobal {
        READ, WRITE;
    }
    

    # DynamicDataSourceHolder
    public final class DynamicDataSourceHolder {
        private static final ThreadLocal holder = new ThreadLocal();
        private DynamicDataSourceHolder() {}
        public static void putDataSource(DynamicDataSourceGlobal dataSource) {
            holder.set(dataSource);
        }
        public static DynamicDataSourceGlobal getDataSource() {
            return holder.get();
        }
        public static void clearDataSource() {
            holder.remove();
        }
    }
    

    3.mybatisのinterceptブロッキング読み書き操作を構成し、SqlCommandTypeに従って読み書きを区別し、データソースを割り当てる
    # DynamicPlugin (intercept)
    @Intercepts({
            @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
    })
    public class DynamicPlugin implements Interceptor {
        protected static final Logger logger = LoggerFactory.getLogger(DynamicPlugin.class);
        private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
        private static final Map cacheMap = new ConcurrentHashMap<>();
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
            //         ,       ,  DynamicDataSourceTransactionManager  
            if (!synchronizationActive) {
                Object[] objects = invocation.getArgs();
                MappedStatement ms = (MappedStatement) objects[0];
                DynamicDataSourceGlobal dynamicDataSourceGlobal = null;
                if ((dynamicDataSourceGlobal = cacheMap.get(ms.getId())) == null) {
                    //   
                    if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                        //!selectKey    id    (SELECT LAST_INSERT_ID() )  ,    
                        if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                            dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                        } else {
                            BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                            String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\
    \\r]", " "); if (sql.matches(REGEX)) { dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE; } else { dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ; } } } else { dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE; } logger.warn(" [{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), dynamicDataSourceGlobal.name(), ms.getSqlCommandType().name()); cacheMap.put(ms.getId(), dynamicDataSourceGlobal); } DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal); } return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } else { return target; } } @Override public void setProperties(Properties properties) {} }

    ここで注意しなければならないのはqueryメソッドのブロックです.Executorではqueryメソッドは2つあります.
     List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
     List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
    

    クエリー文として注釈とxmlを同時に使用しているため、ここでは両方を構成します.
    #DynamicDataSourceConfig(InterceptをSqlSessionFactoryに転送)
        @Bean
        public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DynamicDataSource dataSource) {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            bean.setPlugins(new DynamicPlugin[]{new DynamicPlugin()});
            try {
                return bean.getObject();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
        @Bean
        public SqlSessionTemplate getSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
            SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory);
            return template;
        }
    

    4.トランザクション状態でのデータ・ソースの割当ての構成
    #D y n a m i c DataSourceTransactionManager(カスタムDataSourceTransactionManager実装割当ロジック)
    public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {
        //       ,       
        @Override
        protected void doBegin(Object transaction, TransactionDefinition definition) {
            //     
            boolean readOnly = definition.isReadOnly();
            if (readOnly) {
                DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ);
            } else {
                DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE);
            }
            super.doBegin(transaction, definition);
        }
        //          
        @Override
        protected void doCleanupAfterCompletion(Object transaction) {
            super.doCleanupAfterCompletion(transaction);
            DynamicDataSourceHolder.clearDataSource();
        }
    

    #D y n amicDataSourceConfig(transactionManagerの構成)
        @Bean
        public DynamicDataSourceTransactionManager getDynamicDataSourceTransactionManager(
                @Qualifier("dataSource") DynamicDataSource dataSource) {
            DynamicDataSourceTransactionManager transactionManager = new DynamicDataSourceTransactionManager();
            transactionManager.setDataSource(dataSource);
            return transactionManager;
        }
    

    ここから読み書き分離の構成は終了しても、基本的にビジネスロジックに侵入することはなく、簡潔明瞭で、wuyongde 0922の共有に再び感謝します.
    5.demoを書いてテストする
    # TestMapper
        @Insert("insert into test(value) values(#{value})")
        @Options(useGeneratedKeys = true, keyColumn = "id")
        int insert(Test test);
    
        @Select("select * from test where id = #{id}")
        Test getById(@Param("id") Long id);
    

    #TestService(オープントランザクション@EnableTransactionManagement)
    @Service
    public class TestService {
        @Autowired
        TestMapper testMapper;
        
        @Transactional
        public void insert(Test test) {
            testMapper.insert(test);
        }
    }
    

    # TestMapperTest
    public class TestMapperTest extends ReadWriteSplittingMybatisApplicationTests {
        @Autowired
        TestMapper testMapper;
        @Autowired
        TestService testService;
    
        @org.junit.Test
        public void insert() {
            System.out.println(testMapper.getById(1l));
            Test test = new Test();
            test.setValue(UUID.randomUUID().toString());
            testService.insert(test);
        }
    
        @org.junit.Test
        public void queryById() {
            Test test = testMapper.getById(1l);
            System.out.println(test);
        }
    

    読み取りライブラリにレコードを手動で挿入します(1,「123」)
    # run test
    # 
    Test{id=1, value='123'}
    # 
    Test{id=1, value='e37ef3b8-7d23-42a8-b445-b99e88409a7b'}
    

    読み書き分離構成完了
    現在の構成に基づいて、複数のライブラリ、ライブラリなど、より多くの拡張を行うことができます.