java-mysql-读写分离

说明

springboot中可以定义多个数据源,通过AbstractRoutingDataSource + AOP可以实现读写分离。
如下示例中:

  • 有三个mysql节点,一个主节点两个从节点。
  • 自定义了一个注解@DBReadOnly,有此方法的注解,表示为只读操作,使用从节点。无此注解的方法,不管是读还是写,均使用主节点。

示例

pom.xml

添加mysql/aop的依赖。

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

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

DbConfiguration

定义了三个数据源、事务管理器、JdbcTemplate。

/**
 * 配置类,创建DataSource/JdbcTemplate/TransactionManager的Bean实例
 */
@Configuration
public class DbConfiguration {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Value("${spring.datasource.read1.url:}")
    private String read1Url;

    @Value("${spring.datasource.read2.url:}")
    private String read2Url;

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.write")
    public DataSourceProperties writeDataSourceProperties() {
        logger.info("create WRITE PROP");
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.write.hikari")
    public HikariDataSource writeDataSource() {
        logger.info("create WRITE DS");
        return writeDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.read1")
    public DataSourceProperties read1DataSourceProperties() {
        if (read1Url != null && read1Url.length() != 0) {
            logger.info("create READ1 PROP");
            return new DataSourceProperties();
        } else {
            logger.info("READ1 use WRITE PROP");
            return writeDataSourceProperties();
        }
    }

    @Bean
    @ConfigurationProperties("spring.datasource.read1.hikari")
    public HikariDataSource read1DataSource() {
        if (read1Url != null && read1Url.length() != 0) {
            logger.info("create READ1 DS");
            return read1DataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
        } else {
            logger.info("READ1 use WRITE DS");
            return writeDataSource();
        }
    }

    @Bean
    @ConfigurationProperties("spring.datasource.read2")
    public DataSourceProperties read2DataSourceProperties() {
        if (read2Url != null && read2Url.length() != 0) {
            logger.info("create READ2 PROP");
            return new DataSourceProperties();
        } else {
            logger.info("READ2 use WRITE PROP");
            return writeDataSourceProperties();
        }
    }

    @Bean
    @ConfigurationProperties("spring.datasource.read2.hikari")
    public HikariDataSource read2DataSource() {
        if (read2Url != null && read2Url.length() != 0) {
            logger.info("create READ2 DS");
            return read2DataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
        } else {
            logger.info("READ2 use WRITE DS");
            return writeDataSource();
        }
    }

    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>(3);
        targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
        targetDataSources.put(DbContextHolder.READ + "1", read1DataSource());
        targetDataSources.put(DbContextHolder.READ + "2", read2DataSource());
        proxy.setDefaultTargetDataSource(writeDataSource());
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        JdbcTemplate jt = new JdbcTemplate();
        jt.setDataSource(routingDataSource());
        return jt;
    }

    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        return new DataSourceTransactionManager(routingDataSource());
    }
}

DbContextHolder

此类中的contextHolder变量,存储着各线程是使用写库,还是使用读库。

public class DbContextHolder {
    private static Logger log = LoggerFactory.getLogger(DbContextHolder.class);
    public static final String WRITE = "write";
    public static final String READ = "read";

    private static ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDbType(String dbType) {
        if (dbType == null) {
            log.error("dbType为空");
            throw new NullPointerException();
        }
        log.info("设置dbType为:{}", dbType);
        contextHolder.set(dbType);
    }

    public static String getDbType() {
        return contextHolder.get() == null ? WRITE : contextHolder.get();
    }

    public static void clearDbType() {
        contextHolder.remove();
        log.info("清除dbType");
    }
}

DBReadOnly

注解类,以此注解标注的方法,使用读库。

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DBReadOnly {
}

DBReadOnlyAspect

切面类,此切面的以上述的DBReadOnly注解为切点。
此注解标注的方法,执行前设置DbContextHolder为使用读库,执行后清除DbContextHolder。

@Aspect
@Component
public class DBReadOnlyAspect {

    @Around("@annotation(DBReadOnly)")
    public Object read(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            DbContextHolder.setDbType(DbContextHolder.READ);
            return joinPoint.proceed();
        } finally {
            DbContextHolder.clearDbType();
        }
    }
}

MyAbstractRoutingDataSource

获取数据源前,会调用此类中的determineCurrentLookupKey()方法,通过此方法返回值,判断使用哪个数据源。

在DbConfiguration中routingDataSource()方法中,可以看到,声明MyAbstractRoutingDataSource的Bean时,向其中设置了一个HashMap,其中key为一个字符串,value为一个数据源。determineCurrentLookupKey()方法的返回值,即为这个HashMap的key,如果此方法返回的key在HashMap中不存在,则会使用默认的数据源。

public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DbContextHolder.getDbType();
        if (typeKey == DbContextHolder.WRITE) {
            logger.info("use WRITE DS");
            return typeKey;
        }
        int random = getRandom();
        logger.info("use READ" + random + " DS");
        return DbContextHolder.READ + String.valueOf(random);
    }

    /**
     * 获取随机数
     * 
     * @return 1或2
     */
    private int getRandom() {
        int result = new Random().nextInt(2) + 1;
        return result;
    }
}

TestDao

数据库操作类,此类中有三个方法,其中有一个方法标注了@DBReadOnly,调用此方法时,会使用读库。

@Component
public class TestDao {
    @Autowired
    private JdbcTemplate jdbc;

    @DBReadOnly
    public List<Map<String, Object>> query() {
        List<Map<String, Object>> result = null;
        String sql = "select ip_addr from test.t_syb";
        result = jdbc.queryForList(sql);
        return result;
    }

    public void insert() {
        String addSql = "insert into test.t_syb values (‘xxx‘)";
        jdbc.update(addSql);
    }

    public void delete() {
        String sql = "delete from test.t_syb where ip_addr=‘xxx‘";
        jdbc.update(sql);
    }
}

TestCtrl

此类是一个RestController,仅是为了测试TestDao方法调用的。

@RestController
public class TestCtrl {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private TestDao testDao;

    @RequestMapping("/query")
    public Object query(@RequestParam(required = false) String type) {
        List<Map<String, Object>> list = null;
        try {
            list = testDao.query();
        } catch (Exception e) {
            logger.error("", e);
        }
        return list;
    }

    @RequestMapping("/insert")
    public Object insert() {
        try {
            testDao.insert();
        } catch (Exception e) {
            logger.error("", e);
        }
        return "OK";
    }

    @RequestMapping("/delete")
    public Object delete() {
        try {
            testDao.delete();
        } catch (Exception e) {
            logger.error("", e);
        }
        return "OK";
    }
}

注意

判断是否使用读库,是通过AOP实现的,而为使AOP生效,一定要使用代理调用,即先获取TestDao的Bean实例,然后通过Bean实例去调用方法。
如果是在TestDao实例内部进行调用,AOP是不会生效的。
spring @Transactional注解也是一样的原理,需要注意。

java-mysql-读写分离

上一篇:数据库复习


下一篇:Mysql 四种事务隔离级别