springboot2.0+druid+mybatis 读写分离

文末有demo链接

先上pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.carlton</groupId>
	<artifactId>Read-Write-database</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>Read-Write-database</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.5.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>

		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>

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

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.11</version>
		</dependency>

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

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

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

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

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

配置文件:

server:
  port: 8080
  tomcat:
    uri-encoding: UTF-8
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://127.0.0.1:3306/trymaster?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123123
    readSize: 1
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    driver-class-name: com.mysql.jdbc.Driver
    filters: stat,wall
    initialSize: 5
    maxActive: 20
    maxPoolPreparedStatementPerConnectionSize: 20
    maxWait: 60000
    minEvictableIdleTimeMillis: 300000
    minIdle: 5
    poolPreparedStatements: true
    testOnBorrow: false
    testOnReturn: false
    testWhileIdle: true
    timeBetweenEvictionRunsMillis: 60000
    useGlobalDataSourceStat: true
    validationQuery: SELECT 1 FROM DUAL
  slave:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://127.0.0.1:3306/tryslave1?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123123
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    driver-class-name: com.mysql.jdbc.Driver
    filters: stat,wall
    initialSize: 5
    maxActive: 20
    maxPoolPreparedStatementPerConnectionSize: 20
    maxWait: 60000
    minEvictableIdleTimeMillis: 300000
    minIdle: 5
    poolPreparedStatements: true
    testOnBorrow: false
    testOnReturn: false
    testWhileIdle: true
    timeBetweenEvictionRunsMillis: 60000
    useGlobalDataSourceStat: true
    validationQuery: SELECT 1 FROM DUAL
  messages:
    encoding: UTF-8
  http:
    encoding:
      force: true
      charset: UTF-8
      enabled: true
mybatis:
  mapper-locations: com/carlton/demo/mapper/*.xml
  type-aliases-package: com.carlton.demo.entity
logging:
  level:
    com.carlton.demo.mapper: debug
  
druid:
  init:
    prefix: /druid/*
    allow: 
    deny: 192.168.1.100
    loginUsername: root
    loginPassword: 123123
    resetEnable: false

下面是切换数据源所需的几个工具类

首先用枚举类型定义数据库的主从(或者多个数据库):DataSourceType.class

package com.carlton.demo.conf.split;

/***
 * 枚举区分读写库
 * 
 * @author Carlton
 *
 */
public enum DataSourceType {
	read("read", "从库"), write("write", "主库");
	private String type;
	private String name;

	DataSourceType(String type, String name) {
		this.type = type;
		this.name = name;
	}

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}

创建一个用于切换数据源的全局变量 DataSourceContextHolder.class

package com.carlton.demo.conf.split;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import lombok.extern.slf4j.Slf4j;

/***
 * 本地线程全局变量
 * 
 * @author Carlton
 *
 */
@Slf4j
public class DataSourceContextHolder {
	static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);

	private static final ThreadLocal<String> local = new ThreadLocal<String>();

	public static ThreadLocal<String> getLocal() {
		return local;
	}

	/**
	 * 读可能是多个库
	 */
	public static void read() {
		local.set(DataSourceType.read.getType());
	}

	/**
	 * 写只有一个库
	 */
	public static void write() {
		log.debug("writewritewrite");
		local.set(DataSourceType.write.getType());
	}

	public static String getJdbcType() {
		return local.get();
	}
}

然后是切入点和切面 DataSourceAop.class

package com.carlton.demo.conf.split;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/***
 * 切换数据源
 * 
 * @author Carlton
 *
 */
@Aspect
@Component
@Slf4j
public class DataSourceAop {

	static Logger log = LoggerFactory.getLogger(DataSourceAop.class);

	@Before("execution(* com.carlton.demo.mapper.*.select*(..)) || execution(* com.carlton.demo.mapper.*.count*(..))")
	public void setReadDataSourceType() {
		DataSourceContextHolder.read();
		log.info("dataSource切换到:Read");
	}

	@Before("execution(* com.carlton.demo.mapper.*.insert*(..)) || execution(* com.carlton.demo.mapper.*.update*(..)) || execution(* com.carlton.demo.mapper.*.delete*(..))")
	public void setWriteDataSourceType() {
		DataSourceContextHolder.write();
		log.info("dataSource切换到:write");
	}
}

 

如果配置了多个从库,即多个读库,则需要一个简单的负载均衡来确定读取哪个从库 MyAbstractRoutingDataSource.class

package com.carlton.demo.conf.split;

import java.util.concurrent.atomic.AtomicInteger;

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

/***
 * 多数据源切换,从库负载均衡
 * 
 * @author Carlton
 *
 */
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {

	private final int dataSourceNumber;
	private AtomicInteger count = new AtomicInteger(0);

	public MyAbstractRoutingDataSource(int dataSourceNumber) {
		this.dataSourceNumber = dataSourceNumber;
	}

	@Override
	protected Object determineCurrentLookupKey() {
		String typeKey = DataSourceContextHolder.getJdbcType();
		if (typeKey.equals(DataSourceType.write.getType()))
			return DataSourceType.write.getType();
		// 读 简单负载均衡
		int number = count.getAndAdd(1);
		int lookupKey = number % dataSourceNumber;
		Integer i = lookupKey;
		return i;
	}

}

然后是 druid 数据库配置类:DataSourceConfiguration.class

package com.carlton.demo.conf.split;

import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

import lombok.extern.slf4j.Slf4j;

/**
 * 数据库配置
 */
@Slf4j
@Configuration
public class DataSourceConfiguration {

	protected Logger log = LoggerFactory.getLogger(DataSourceConfiguration.class);

	@Value("${spring.datasource.type}")
	private Class<? extends DataSource> dataSourceType;
	@Value("${druid.init.prefix}")
	private String prefix;
	@Value("${druid.init.allow}")
	private String allow;
	@Value("${druid.init.deny}")
	private String deny;
	@Value("${druid.init.loginUsername}")
	private String loginUsername;
	@Value("${druid.init.loginPassword}")
	private String loginPassword;
	@Value("${druid.init.resetEnable}")
	private String resetEnable;

	@Bean
	public ServletRegistrationBean druidServlet() {
		log.info("init Druid Servlet Configuration ");
		ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), prefix);
		// IP白名单
		servletRegistrationBean.addInitParameter("allow", allow);
		// IP黑名单(共同存在时,deny优先于allow)
		servletRegistrationBean.addInitParameter("deny", deny);
		// 控制台管理用户
		servletRegistrationBean.addInitParameter("loginUsername", loginUsername);
		servletRegistrationBean.addInitParameter("loginPassword", loginPassword);
		// 是否能够重置数据 禁用HTML页面上的“Reset All”功能
		servletRegistrationBean.addInitParameter("resetEnable", resetEnable);
		return servletRegistrationBean;

	}

	@Bean
	public FilterRegistrationBean filterRegistrationBean() {
		FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
		filterRegistrationBean.addUrlPatterns("/*");
		filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico," + prefix);
		return filterRegistrationBean;
	}

	@Bean(name = "writeDataSource", destroyMethod = "close", initMethod = "init")
	@Primary
	@ConfigurationProperties(prefix = "spring.datasource")
	public DataSource writeDataSource() {
		log.info("-------------------- writeDataSource init ---------------------");
		return DataSourceBuilder.create().type(dataSourceType).build();
	}

	/**
	 * 有多少个从库就要配置多少个
	 * 
	 * @return
	 */
	@Bean(name = "readDataSource1")
	@ConfigurationProperties(prefix = "spring.slave")
	public DataSource readDataSourceOne() {
		log.info("-------------------- readDataSourceOne init ---------------------");
		return DataSourceBuilder.create().type(dataSourceType).build();
	}

	@Bean("readDataSources")
	public List<DataSource> readDataSources() {
		List<DataSource> dataSources = new ArrayList<>();
		dataSources.add(readDataSourceOne());
		return dataSources;
	}
}

最后是配置事务管理 DataSourceTransactionManager.class

package com.carlton.demo.conf.split;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import lombok.extern.slf4j.Slf4j;

/***
 * 自定义事务
 * 
 * @author Carlton
 *
 */
@Configuration
@EnableTransactionManagement
@Slf4j
public class DataSourceTransactionManager extends DataSourceTransactionManagerAutoConfiguration {

	static Logger log = LoggerFactory.getLogger(DataSourceTransactionManager.class);

	@Resource(name = "writeDataSource")
	private DataSource dataSource;

	/**
	 * 自定义事务
	 * MyBatis自动参与到spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与DataSourceTransactionManager引用的数据源一致即可,否则事务管理会不起作用。
	 * 
	 * @return
	 */
	@Bean(name = "transactionManager")
	public org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManagers() {
		log.info("-------------------- transactionManager init ---------------------");
		return new org.springframework.jdbc.datasource.DataSourceTransactionManager(dataSource);
	}
}

到这里,读写分离主要步骤都已经完成,下面就是建表写测试代码:

在两个库中新建两张一样的表,user表,实体类如下:

package com.carlton.demo.entity;

public class User {
    private Integer id;

    private String name;

    private Integer age;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name == null ? null : name.trim();
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

-----mapper 类 和 xml ----省略,

再然后新建controller和service,写测试代码。

注意要使事务管理生效,需在相应service方法上加上@Transactional 注解

完整demo:

https://gitee.com/carltonq/read_write_separation_demo.git

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值