decadence

個人のメモ帳

spring-txでReplication対応

以下の環境において、DBにmasterとslaveがある場合にリクエストを振り分ける

  • Transaction宣言: spring-tx
  • Datasource: commons-dbcp, BoneCP
  • ORM: spring-jpa, MyBatis

MySQLレプリケーションをVagrantで試してみる。 - Qiita で作ったReplication環境で、general_logを見ながらリクエストがどちらに飛んでいるかを確認出来る。

基本: spring-jpa

spring-bootでさっと確かめてる

  • application.prperties
    • replication driverでmasterとslaveを指定
spring.datasource.url=jdbc:mysql:replication://192.168.33.10,192.168.33.11/sample
spring.datasource.driverClassName=com.mysql.jdbc.Driver
  • repository層 ⇒ service層 ⇒ controller層 という仕組みを作る

UserRepository.java (サンプルなのでJpaRepository)

@Repository
public interface UserRepository extends JpaRepository<User, Integer>{}

UserService.java

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public List<User> findAll() {
        return userRepository.findAll(); // slaveに投げられる
    }

    @Transactional(readOnly = false)
    public User create(String name, int age) {
        return userRepository.saveAndFlush(new User(name, age)); // masterに投げられる
    }
}

UserControllerUserServiceを利用して、如何なるrepositoryも触らない。create内で例外が投げられた際にはrollbackされる。

spring-jpaの場合、repositoryのメソッドの中でコネクションをDataSourceから獲得しようとする。その前にTransactionalにより明示的にSessionが作成されていたら、その情報を元にコネクションを取りに行くため、ReplicationDriverにより振り分けられたmasterとslaveへのコネクションがDataSourceからreadOnly(default=true)の値を元に渡される

DataSourceがDBCPからHikariCPなどになっていても挙動は変わらない。試しにこちらを参考にBoneCPに差し替えても特に問題なく、master/slaveへリクエストが振り分けられた。

MyBatis編

pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
        </dependency>

MyBatisConfig.java

@MapperScan("com.krrrr38.sample.repository")
public class MyBatisConfig {
    ...datasource....

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource());
        sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("/sql/mapper-config.xml"));
        sqlSessionFactoryBean.setFailFast(true);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public DataSourceTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }

resources/sql/mapper-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <mappers>
        <mapper resource="sql/UserRepository.xml"/>
    </mappers>
</configuration>

UserService.java

@Service
@Transactional(readOnly = true) // !!!!!!!!!基本はslaveに投げるようにこれが必要!!!!!!!!!
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public List<User> findAll() {
        return userRepository.findAll(); // slaveに投げられる
    }

    @Transactional(readOnly = false) // masterに投げたいとこだけ、readOnly=falseなTransactionalを付ける
    public User create(String name, int age) {
        return userRepository.saveAndFlush(new User(name, age)); // masterに投げられる
    }
}

MyBatisもreplicationDriver使ってる時はデフォルトでreadOnly=trueになって欲しいけど、そもそもレイアが違うのにどうなってんだjpa

AOPで対応したい時

@Transactional使うとautocommit=onになってしまうので、AOPの方が良いかも

このあたりが参考になる