mysql 读写分离 事务6_读写分离注解(示例代码)

本文档详细介绍了如何实现MySQL的读写分离,包括配置概述、程序说明、数据源配置、主从库配置、数据池配置、MyBatis配置、事务管理及注解实现。通过AbstractRoutingDataSource和ThreadLocal实现动态数据源选择,同时讨论了这种方法的优缺点,如无法动态增加数据源和主从延迟问题。

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

以前写过读写分离,今天完善成文档。

一:概述

1.结构文档

20200430171426843456.png

2.思路

组装好各个数据源,然后通过注解选择使用读或者写的数据源,将其使用AbstractRoutingDataSource中的方法determineCurrentLookuoKey进行选择datasource的key。

然后,通过key,就找到了要使用的数据源。

在数据源的这里,最后配置上连接池。

3.说明

本配置直接使用一个公共的连接池配置,如果需要,自己进行配置

二:程序说明

1.连接池配置

package com.jun.webpro.common.config.dataSource.properties;

import lombok.Data;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.stereotype.Component;

import java.util.List;

import java.util.Map;

/**

* 数据库连接池配置

*/

@ConfigurationProperties(prefix = "spring.druid")

@Component

@Data

public class DruidProperties {

/**

* 初始化大小

*/

private int initialSize;

/**

* 最小

*/

private int minIdle;

/**

* 最大

*/

private int maxActive;

/**

* 获取连接等待超时的时间

*/

private int maxWait;

/**

* 间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒

*/

private int timeBetweenEvictionRunsMillis;

/**

* 一个连接在池中最小生存的时间,单位是毫秒

*/

private int minEvictableIdleTimeMillis;

private String validationQuery;

private boolean testWhileIdle;

private boolean testOnBorrow;

private boolean testOnReturn;

/**

* 打开PSCache

*/

private boolean poolPreparedStatements;

/**

* 并且指定每个连接上PSCache的大小

*/

private int maxPoolPreparedStatementPerConnectionSize;

/**

* 监控统计拦截的filters,去掉后监控界面sql无法统计,‘wall‘用于防火墙

*/

private String filters;

/**

* 物理连接初始化的时候执行的sql

*/

private List connectionInitSqls;

/**

* connectProperties属性来打开mergeSql功能;慢SQL记录

*/

private Map connectionProperties;

}

2.主从库的配置项接口

因为通过这个接口,进行下面的注入

package com.jun.webpro.common.config.dataSource.properties;

public interface DataSourceProperties {

String getDriverClassName();

String getUrl();

String getUsername();

String getPassword();

}

3.从库配置

package com.jun.webpro.common.config.dataSource.properties;

import lombok.Data;

import lombok.Getter;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.stereotype.Component;

@Component

@Data

@Getter

@ConfigurationProperties("mysql.datasource.slave")

public class SlaveDataSourceProperties implements DataSourceProperties{

/**

* url

*/

private String url;

/**

* driverClassName

*/

private String driverClassName;

/**

* username

*/

private String username;

/**

* password

*/

private String password;

}

4.主库配置

package com.jun.webpro.common.config.dataSource.properties;

import lombok.Data;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.stereotype.Component;

@Component

@Data

@ConfigurationProperties("mysql.datasource.master")

public class MasterDataSourceProperties implements DataSourceProperties{

/**

* url

*/

private String url;

/**

* driverClassName

*/

private String driverClassName;

/**

* username

*/

private String username;

/**

* password

*/

private String password;

}

5.数据池配置项

package com.jun.webpro.common.config.dataSource.config;

import com.alibaba.druid.pool.DruidDataSource;

import com.jun.webpro.common.config.dataSource.properties.DataSourceProperties;

import com.jun.webpro.common.config.dataSource.properties.DruidProperties;

import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

import java.sql.SQLException;

import java.util.Properties;

@Configuration

public class DataSourceConfig {

// 导入数据库连接池配置

@Resource

private DruidProperties druidProperties;

/**

* @param dataSourceProperties 其他地方传参

*/

DruidDataSource initDruidDataSource(DataSourceProperties dataSourceProperties) throws SQLException {

try (DruidDataSource dataSource = new DruidDataSource()) {

dataSource.setInitialSize(druidProperties.getInitialSize());

dataSource.setMinIdle(druidProperties.getMinIdle());

dataSource.setMaxActive(druidProperties.getMaxActive());

dataSource.setMaxWait(druidProperties.getMaxWait());

dataSource.setTimeBetweenEvictionRunsMillis(druidProperties.getTimeBetweenEvictionRunsMillis());

dataSource.setMinEvictableIdleTimeMillis(druidProperties.getMinEvictableIdleTimeMillis());

dataSource.setValidationQuery(druidProperties.getValidationQuery());

dataSource.setTestWhileIdle(druidProperties.isTestWhileIdle());

dataSource.setTestOnBorrow(druidProperties.isTestOnBorrow());

dataSource.setTestOnReturn(druidProperties.isTestOnReturn());

dataSource.setPoolPreparedStatements(druidProperties.isPoolPreparedStatements());

dataSource.setMaxPoolPreparedStatementPerConnectionSize(druidProperties.getMaxPoolPreparedStatementPerConnectionSize());

dataSource.setConnectionInitSqls(druidProperties.getConnectionInitSqls());

dataSource.setFilters(druidProperties.getFilters());

Properties properties = new Properties();

for (String key : druidProperties.getConnectionProperties().keySet()) {

properties.setProperty(key, druidProperties.getConnectionProperties().get(key));

}

dataSource.setConnectProperties(properties);

dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());

dataSource.setUrl(dataSourceProperties.getUrl());

dataSource.setUsername(dataSourceProperties.getUsername());

dataSource.setPassword(dataSourceProperties.getPassword());

return dataSource;

}

}

}

6.主数据库配置项

package com.jun.webpro.common.config.dataSource.config;

import com.jun.webpro.common.config.dataSource.properties.MasterDataSourceProperties;

import lombok.extern.slf4j.Slf4j;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import javax.annotation.Resource;

import javax.sql.DataSource;

import java.sql.SQLException;

@Slf4j

@Configuration

public class MasterDataSourceConfig extends DataSourceConfig{

@Resource

private MasterDataSourceProperties dataSourceProperties;

/**

* 数据源

*/

@Bean(name = "masterDataSource")

@Primary

public DataSource masterDataSource() throws SQLException {

return initDruidDataSource(dataSourceProperties);

}

}

7.从数据库配置

package com.jun.webpro.common.config.dataSource.config;

import com.jun.webpro.common.config.dataSource.properties.MasterDataSourceProperties;

import com.jun.webpro.common.config.dataSource.properties.SlaveDataSourceProperties;

import lombok.extern.slf4j.Slf4j;

import org.apache.ibatis.session.SqlSessionFactory;

import org.mybatis.spring.SqlSessionFactoryBean;

import org.mybatis.spring.annotation.MapperScan;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.annotation.Resource;

import javax.sql.DataSource;

import java.sql.SQLException;

@Slf4j

@Configuration

public class SlaveDataSourceConfig extends DataSourceConfig{

@Resource

private SlaveDataSourceProperties dataSourceProperties;

/**

* 数据源

*/

@Bean(name = "slaveDataSource")

public DataSource masterDataSource() throws SQLException {

return initDruidDataSource(dataSourceProperties);

}

}

8.mybatis配置项

这里用于mysql事务

package com.jun.webpro.common.config.dataSource.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

public class MybatisConfig {

@Bean("mybatis-config")

@ConfigurationProperties(prefix = "mybatis.configuration")

public org.apache.ibatis.session.Configuration globalConfiguration() {

return new org.apache.ibatis.session.Configuration();

}

}

9.将数据源的key设置进ThreadLocal中

这里主要是注解中,将值添加进来,然后后面使用。

package com.jun.webpro.common.config.dataSource.route;

import lombok.extern.slf4j.Slf4j;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

/**

* Description 这里切换读/写模式

* 原理是利用ThreadLocal保存当前线程是否处于读模式(通过开始READ_ONLY注解在开始操作前设置模式为读模式,

* 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式

*/

@Slf4j

public class DbContextHolder {

public static final String MASTER = "masterDataSource";

public static final String SLAVE = "slaveDataSource";

private static ThreadLocal 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();

}

public static void clearDbType() {

contextHolder.remove();

}

}

10.去ThreadLocal中获取荣国注解加入的key

这个key可以决定走哪个库。

package com.jun.webpro.common.config.dataSource.route;

import com.jun.webpro.common.config.dataSource.route.DbContextHolder;

import com.jun.webpro.common.units.NumberUtils;

import lombok.extern.slf4j.Slf4j;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.Objects;

/**

* Description

*

*/

@Slf4j

public class RoutingDataSource extends AbstractRoutingDataSource {

@Value("${mysql.datasource.num}")

private int num;

@Override

protected Object determineCurrentLookupKey() {

String typeKey = DbContextHolder.getDbType();

if (Objects.equals(DbContextHolder.MASTER, typeKey)) {

log.info("使用了写库");

return typeKey;

}else {

int sum = NumberUtils.getRandom(1, num);

log.info("使用了读库{}", sum);

return DbContextHolder.SLAVE + sum;

}

}

}

11.读写配置,主要点是重写routingDataSource

package com.jun.webpro.common.config.dataSource.route;

import com.jun.webpro.common.config.dataSource.config.DataSourceConfig;

import com.jun.webpro.common.config.dataSource.route.DbContextHolder;

import com.jun.webpro.common.config.dataSource.route.RoutingDataSource;

import org.apache.ibatis.session.SqlSessionFactory;

import org.mybatis.spring.SqlSessionFactoryBean;

import org.mybatis.spring.annotation.MapperScan;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.annotation.Resource;

import javax.sql.DataSource;

import java.util.HashMap;

import java.util.Map;

/**

* Description

*

*/

@Configuration

@MapperScan(basePackages = "com.jun.webpro.common.domain.mapper", sqlSessionFactoryRef = "sqlSessionFactory")

public class WriteOrReadDatabaseConfig extends DataSourceConfig {

@Resource

private DataSource masterDataSource;

@Resource

private DataSource slaveDataSource;

/**

* 事务

*/

@Bean

public DataSourceTransactionManager masterTransactionManager() {

return new DataSourceTransactionManager(routingDataSource());

}

@Bean

public SqlSessionFactory sqlSessionFactory(@Qualifier("mybatis-config") org.apache.ibatis.session.Configuration configuration) throws Exception {

final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();

sessionFactory.setDataSource(routingDataSource());

sessionFactory.setConfiguration(configuration);

sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));

return sessionFactory.getObject();

}

/**

* 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源

* 工厂模式

*/

@Bean

public AbstractRoutingDataSource routingDataSource() {

RoutingDataSource proxy = new RoutingDataSource();

Map targetDataSources = new HashMap<>(2);

targetDataSources.put(DbContextHolder.MASTER, masterDataSource);

targetDataSources.put(DbContextHolder.SLAVE+"1", slaveDataSource);

proxy.setDefaultTargetDataSource(slaveDataSource);

proxy.setTargetDataSources(targetDataSources);

return proxy;

}

}

12.注解

package com.jun.webpro.common.aspect;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

/**

* Description 通过该接口注释的service使用读模式,其他使用写模式

*

* 接口注释只是一种办法,如果项目已经有代码了,通过注释可以不修改任何业务代码加持读写分离

* 也可以通过切面根据方法开头来设置读写模式,例如getXXX()使用读模式,其他使用写模式

*

*/

@Target({ElementType.METHOD,ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

public @interface ReadOnly {

}

13,注解实现

将要使用的库写进去

package com.jun.webpro.common.aspect.impl;

import com.jun.webpro.common.aspect.ReadOnly;

import com.jun.webpro.common.config.dataSource.route.DbContextHolder;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.core.Ordered;

import org.springframework.stereotype.Component;

/**

* Description

*

*/

@Aspect

@Component

public class ReadOnlyInterceptor implements Ordered {

private static final Logger log= LoggerFactory.getLogger(ReadOnlyInterceptor.class);

@Around("@annotation(readOnly)")

public Object setRead(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable{

try{

// 通过注解,将值注册进去

DbContextHolder.setDbType(DbContextHolder.SLAVE);

return joinPoint.proceed();

}finally {

DbContextHolder.clearDbType();

log.info("清除threadLocal");

}

}

@Override

public int getOrder() {

return 0;

}

}

三:配置项

1.

mysql:

datasource:

#读库数目

num: 1

master:

url: jdbc:mysql://47.103.25.1:3306/center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=UTC

username: root

password: 123456

driver-class-name: com.mysql.cj.jdbc.Driver

slave:

url: jdbc:mysql://47.103.25.1:3306/center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=UTC

username: root

password: 123456

driver-class-name: com.mysql.cj.jdbc.Driver

spring:

# Redis配置, 使用了连接池

redis:

database: 0

host: 106.14.25.1

port: 6379

password:

jedis:

pool:

max-active: 20

max-wait: -1

max-idle: 20

min-idle: 10

timeout: 1000

#druid数据库连接池配置

druid:

initialSize: 5

minIdle: 5

maxActive: 8

maxWait: 60000

timeBetweenEvictionRunsMillis: 60000

minEvictableIdleTimeMillis: 300000

validationQuery: SELECT 1

testWhileIdle: true

testOnBorrow: false

testOnReturn: false

poolPreparedStatements: true

maxPoolPreparedStatementPerConnectionSize: 20

connectionInitSqls: set names utf8mb4

filters: stat

connectionProperties:

druid:

stat:

mergeSql: true

slowSqlMillis: 1000

四:关于事务

1.DataSourceTranceManager

AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行。

spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。

将事务管理在数据持久 (Dao层) 开启,切换数据源的操作放在业务层进行操作,就可在事务开启之前顺利进行数据源切换,不会再出现切换失败了。

五:缺点

1.不能动态的增加数据源

2.主从延迟问题

如果往主库增加数据,马上到从库读取,一般没有问题

但是数据量大的时候,因为有延迟,可能去查询的时候,查询不到

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值