адрес проектаGitHub.com/HelloWorld E…
Этот проект использует Spring Boot и MyBatis для реализации переключения нескольких источников данных и источников динамических данных, существует много различных методов реализации В процессе обучения выяснилось, что нет статьи, посвященной этим методам и общим проблемам в централизованно, поэтому часто используемые методы и часто задаваемые вопросы написаны в разных ветках этого проекта:
- master: использование интерфейса RESTful API нескольких источников данных, использование Druid для динамического переключения источников данных на уровне DAO и балансировки нагрузки источников данных только для чтения.
- dev: Динамическое переключение источников данных реализовано простейшим методом аспектов и аннотаций.
- druid: Динамическое переключение источника данных с использованием пула соединений Druid с помощью аспектов и аннотаций.
- aspect_dao: Динамическое переключение источника данных уровня DAO, реализованное через аспекты.
- roundrobin: Балансировка нагрузки источников данных только для чтения с использованием циклического перебора через аспекты.
Все вышеперечисленные ветки изменены или расширены на основе ветки dev и в основном охватывают часто используемые методы динамического переключения нескольких источников данных.
В процессе использования я в основном наступил на все ямки динамического переключения источников данных, а некоторые распространенные ямки и решения написал наIssuesв
В этом проекте используется источник данных с возможностью записи и несколько источников данных только для чтения. Чтобы уменьшить нагрузку на базу данных, для выбора источников данных только для чтения используется метод циклического перебора, учитывая, что в службе будут выполняться операции чтения и записи. , это приложение использует аспект AOP для переключения источника данных только для чтения через имя метода уровня DAO; но этот метод требует, чтобы источник данных был согласован с ведущим и подчиненным, и следует избегать запросов сразу после записи в тот же сервисный метод, если он должен быть выполнен после операции записи Read немедленно, должен быть добавлен в сервисный метод
@Transactional
Аннотация, чтобы убедиться, что используется основной источник данных
Следует отметить, что после использования аспекта уровня DAO его не следует добавлять на уровень класса обслуживания.
@Transactional
Аннотация, но следует добавить метод, который также рекомендуется Spring
Динамическое переключение зависимостей источников данных
configuration
В пакете реализованы четыре класса, а именно:
- DataSourceRoutingDataSource.java
- DataSourceConfigurer.java
- DynamicDataSourceContextHolder.java
- DynamicDataSourceAspect.java
добавить зависимости
dependencies {
compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-aop')
compile('com.alibaba:druid-spring-boot-starter:1.1.6')
runtime('mysql:mysql-connector-java')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Создавать базы данных и таблицы
- Создайте базу данных отдельно
product_master
,product_slave_alpha
,product_slave_beta
,product_slave_gamma
- Создайте таблицы в вышеуказанных базах данных отдельно
product
, и вставьте разные данные
DROP DATABASE IF EXISTS product_master;
CREATE DATABASE product_master;
CREATE TABLE product_master.product(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
price DOUBLE(10,2) NOT NULL DEFAULT 0);
INSERT INTO product_master.product (name, price) VALUES('master', '1');
DROP DATABASE IF EXISTS product_slave_alpha;
CREATE DATABASE product_slave_alpha;
CREATE TABLE product_slave_alpha.product(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
price DOUBLE(10,2) NOT NULL DEFAULT 0);
INSERT INTO product_slave_alpha.product (name, price) VALUES('slaveAlpha', '1');
DROP DATABASE IF EXISTS product_slave_beta;
CREATE DATABASE product_slave_beta;
CREATE TABLE product_slave_beta.product(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
price DOUBLE(10,2) NOT NULL DEFAULT 0);
INSERT INTO product_slave_beta.product (name, price) VALUES('slaveBeta', '1');
DROP DATABASE IF EXISTS product_slave_gamma;
CREATE DATABASE product_slave_gamma;
CREATE TABLE product_slave_gamma.product(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
price DOUBLE(10,2) NOT NULL DEFAULT 0);
INSERT INTO product_slave_gamma.product (name, price) VALUES('slaveGamma', '1');
Настроить источник данных
- application.properties
# Master datasource config
spring.datasource.druid.master.name=master
spring.datasource.druid.master.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.master.url=jdbc:mysql://localhost/product_master?useSSL=false
spring.datasource.druid.master.port=3306
spring.datasource.druid.master.username=root
spring.datasource.druid.master.password=123456
# SlaveAlpha datasource config
spring.datasource.druid.slave-alpha.name=SlaveAlpha
spring.datasource.druid.slave-alpha.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.slave-alpha.url=jdbc:mysql://localhost/product_slave_alpha?useSSL=false
spring.datasource.druid.slave-alpha.port=3306
spring.datasource.druid.slave-alpha.username=root
spring.datasource.druid.slave-alpha.password=123456
# SlaveBeta datasource config
spring.datasource.druid.slave-beta.name=SlaveBeta
spring.datasource.druid.slave-beta.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.slave-beta.url=jdbc:mysql://localhost/product_slave_beta?useSSL=false
spring.datasource.druid.slave-beta.port=3306
spring.datasource.druid.slave-beta.username=root
spring.datasource.druid.slave-beta.password=123456
# SlaveGamma datasource config
spring.datasource.druid.slave-gamma.name=SlaveGamma
spring.datasource.druid.slave-gamma.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.slave-gamma.url=jdbc:mysql://localhost/product_slave_gamma?useSSL=false
spring.datasource.druid.slave-gamma.port=3306
spring.datasource.druid.slave-gamma.username=root
spring.datasource.druid.slave-gamma.password=123456
# Druid dataSource config
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.initial-size=5
spring.datasource.druid.max-active=20
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-wait=60000
spring.datasource.druid.pool-prepared-statements=false
spring.datasource.druid.validation-query=SELECT 1
spring.datasource.druid.validation-query-timeout=30000
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
#spring.datasource.druid.time-between-eviction-runs-millis=
#spring.datasource.druid.min-evictable-idle-time-millis=
#spring.datasource.druid.max-evictable-idle-time-millis=10000
# Druid stat filter config
spring.datasource.druid.filters=stat,wall,log4j
spring.datasource.druid.web-stat-filter.enabled=true
spring.datasource.druid.web-stat-filter.url-pattern=/*
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
spring.datasource.druid.web-stat-filter.session-stat-enable=true
spring.datasource.druid.web-stat-filter.session-stat-max-count=10
spring.datasource.druid.web-stat-filter.principal-session-name=user
#spring.datasource.druid.web-stat-filter.principal-cookie-name=
spring.datasource.druid.web-stat-filter.profile-enable=true
spring.datasource.druid.filter.stat.db-type=mysql
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=1000
spring.datasource.druid.filter.stat.merge-sql=true
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.config.delete-allow=true
spring.datasource.druid.filter.wall.config.drop-table-allow=false
spring.datasource.druid.filter.slf4j.enabled=true
# Druid manage page config
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
spring.datasource.druid.stat-view-servlet.reset-enable=true
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
#spring.datasource.druid.stat-view-servlet.allow=
#spring.datasource.druid.stat-view-servlet.deny=
spring.datasource.druid.use-global-data-source-stat=true
# Druid AOP config
spring.datasource.druid.aop-patterns=cn.com.hellowood.dynamicdatasource.service.*
spring.aop.proxy-target-class=true
# MyBatis config
mybatis.type-aliases-package=cn.com.hellowood.dynamicdatasource.mapper
mybatis.mapper-locations=mappers/**Mapper.xml
server.port=9999
Настроить источник данных
- DataSourceKey.java
package cn.com.hellowood.dynamicdatasource.common;
public enum DataSourceKey {
master,
slaveAlpha,
slaveBeta,
slaveGamma
}
- DataSourceRoutingDataSource.java
Этот класс наследуется от
AbstractRoutingDataSource
класс, который будет вызываться при обращении к БДdetermineCurrentLookupKey()
способ получить ключ экземпляра базы данных
package cn.com.hellowood.dynamicdatasource.configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected Object determineCurrentLookupKey() {
logger.info("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey());
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
- DataSourceConfigurer.java
Класс конфигурации источника данных, в котором несколько экземпляров источника данных генерируются и внедряются в
ApplicationContext
середина
package cn.com.hellowood.dynamicdatasource.configuration;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfigurer {
/**
* master DataSource
* @Primary 注解用于标识默认使用的 DataSource Bean,因为有5个 DataSource Bean,该注解可用于 master
* 或 slave DataSource Bean, 但不能用于 dynamicDataSource Bean, 否则会产生循环调用
*
* @ConfigurationProperties 注解用于从 application.properties 文件中读取配置,为 Bean 设置属性
* @return data source
*/
@Bean("master")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public DataSource master() {
return DruidDataSourceBuilder.create().build();
}
/**
* Slave alpha data source.
*
* @return the data source
*/
@Bean("slaveAlpha")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave-alpha")
public DataSource slaveAlpha() {
return DruidDataSourceBuilder.create().build();
}
/**
* Slave beta data source.
*
* @return the data source
*/
@Bean("slaveBeta")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave-beta")
public DataSource slaveBeta() {
return DruidDataSourceBuilder.create().build();
}
/**
* Slave gamma data source.
*
* @return the data source
*/
@Bean("slaveGamma")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave-gamma")
public DataSource slaveGamma() {
return DruidDataSourceBuilder.create().build();
}
/**
* Dynamic data source.
*
* @return the data source
*/
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(4);
dataSourceMap.put(DataSourceKey.master.name(), master());
dataSourceMap.put(DataSourceKey.slaveAlpha.name(), slaveAlpha());
dataSourceMap.put(DataSourceKey.slaveBeta.name(), slaveBeta());
dataSourceMap.put(DataSourceKey.slaveGamma.name(), slaveGamma());
// 将 master 数据源作为默认指定的数据源
dynamicRoutingDataSource.setDefaultTargetDataSource(master());
// 将 master 和 slave 数据源作为指定的数据源
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
// 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
// 将 Slave 数据源的 key 放在集合中,用于轮循
DynamicDataSourceContextHolder.slaveDataSourceKeys.addAll(dataSourceMap.keySet());
DynamicDataSourceContextHolder.slaveDataSourceKeys.remove(DataSourceKey.master.name());
return dynamicRoutingDataSource;
}
/**
* 配置 SqlSessionFactoryBean
* @ConfigurationProperties 在这里是为了将 MyBatis 的 mapper 位置和持久层接口的别名设置到
* Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
*
* @return the sql session factory bean
*/
@Bean
@ConfigurationProperties(prefix = "mybatis")
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}
/**
* 注入 DataSourceTransactionManager 用于事务管理
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}
- DynamicDataSourceContextHolder.java
Этот класс настроен для контекста источника данных и используется для переключения источников данных.
package cn.com.hellowood.dynamicdatasource.configuration;
import cn.com.hellowood.dynamicdatasource.common.DataSourceKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DynamicDataSourceContextHolder {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* 用于在切换数据源时保证不会被其他线程修改
*/
private static Lock lock = new ReentrantLock();
/**
* 用于轮循的计数器
*/
private static int counter = 0;
/**
* Maintain variable for every thread, to avoid effect other thread
*/
private static final ThreadLocal<Object> CONTEXT_HOLDER = ThreadLocal.withInitial(DataSourceKey.master);
/**
* All DataSource List
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* The constant slaveDataSourceKeys.
*/
public static List<Object> slaveDataSourceKeys = new ArrayList<>();
/**
* To switch DataSource
*
* @param key the key
*/
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
/**
* Use master data source.
*/
public static void useMasterDataSource() {
CONTEXT_HOLDER.set(DataSourceKey.master);
}
/**
* 当使用只读数据源时通过轮循方式选择要使用的数据源
*/
public static void useSlaveDataSource() {
lock.lock();
try {
int datasourceKeyIndex = counter % slaveDataSourceKeys.size();
CONTEXT_HOLDER.set(String.valueOf(slaveDataSourceKeys.get(datasourceKeyIndex)));
counter++;
} catch (Exception e) {
logger.error("Switch slave datasource failed, error message is {}", e.getMessage());
useMasterDataSource();
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* Get current DataSource
*
* @return data source key
*/
public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}
/**
* To set DataSource as default
*/
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
/**
* Check if give DataSource is in current DataSource list
*
* @param key the key
* @return boolean boolean
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
}
- DynamicDataSourceAspect.java
Аспект динамического переключения источников данных отсекает уровень DAO и определяет, какой источник данных использовать с помощью имени метода уровня DAO для реализации переключения источников данных. Порядок аспекта можно опустить, т.к.
@Transactional
является самым низким, в зависимости от настроек других аспектов, иorg.springframework.core.annotation.AnnotationAwareOrderComparator
будет переупорядочен
package cn.com.hellowood.dynamicdatasource.configuration;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
private final String[] QUERY_PREFIX = {"select"};
@Pointcut("execution( * cn.com.hellowood.dynamicdatasource.mapper.*.*(..))")
public void daoAspect() {
}
@Before("daoAspect()")
public void switchDataSource(JoinPoint point) {
Boolean isQueryMethod = isQueryMethod(point.getSignature().getName());
if (isQueryMethod) {
DynamicDataSourceContextHolder.useSlaveDataSource();
logger.info("Switch DataSource to [{}] in Method [{}]",
DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
}
}
@After("daoAspect())")
public void restoreDataSource(JoinPoint point) {
DynamicDataSourceContextHolder.clearDataSourceKey();
logger.info("Restore DataSource to [{}] in Method [{}]",
DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
}
private Boolean isQueryMethod(String methodName) {
for (String prefix : QUERY_PREFIX) {
if (methodName.startsWith(prefix)) {
return true;
}
}
return false;
}
}
Настройка интерфейса REST API продукта
- ProductController.java
package cn.com.hellowood.dynamicdatasource.controller;
import cn.com.hellowood.dynamicdatasource.common.CommonResponse;
import cn.com.hellowood.dynamicdatasource.common.ResponseUtil;
import cn.com.hellowood.dynamicdatasource.modal.Product;
import cn.com.hellowood.dynamicdatasource.service.ProductService;
import cn.com.hellowood.dynamicdatasource.utils.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/{id}")
public CommonResponse getProduct(@PathVariable("id") Long productId) throws ServiceException {
return ResponseUtil.generateResponse(productService.select(productId));
}
@GetMapping
public CommonResponse getAllProduct() {
return ResponseUtil.generateResponse(productService.getAllProduct());
}
@PutMapping("/{id}")
public CommonResponse updateProduct(@PathVariable("id") Long productId, @RequestBody Product newProduct) throws ServiceException {
return ResponseUtil.generateResponse(productService.update(productId, newProduct));
}
@DeleteMapping("/{id}")
public CommonResponse deleteProduct(@PathVariable("id") long productId) throws ServiceException {
return ResponseUtil.generateResponse(productService.delete(productId));
}
@PostMapping
public CommonResponse addProduct(@RequestBody Product newProduct) throws ServiceException {
return ResponseUtil.generateResponse(productService.add(newProduct));
}
}
- ProductService.java
package cn.com.hellowood.dynamicdatasource.service;
import cn.com.hellowood.dynamicdatasource.mapper.ProductDao;
import cn.com.hellowood.dynamicdatasource.modal.Product;
import cn.com.hellowood.dynamicdatasource.utils.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
public Product select(long productId) throws ServiceException {
Product product = productDao.select(productId);
if (product == null) {
throw new ServiceException("Product:" + productId + " not found");
}
return product;
}
@Transactional(rollbackFor = DataAccessException.class)
public Product update(long productId, Product newProduct) throws ServiceException {
if (productDao.update(newProduct) <= 0) {
throw new ServiceException("Update product:" + productId + "failed");
}
return newProduct;
}
@Transactional(rollbackFor = DataAccessException.class)
public boolean add(Product newProduct) throws ServiceException {
Integer num = productDao.insert(newProduct);
if (num <= 0) {
throw new ServiceException("Add product failed");
}
return true;
}
@Transactional(rollbackFor = DataAccessException.class)
public boolean delete(long productId) throws ServiceException {
Integer num = productDao.delete(productId);
if (num <= 0) {
throw new ServiceException("Delete product:" + productId + "failed");
}
return true;
}
public List<Product> getAllProduct() {
return productDao.getAllProduct();
}
}
- ProductDao.java
package cn.com.hellowood.dynamicdatasource.mapper;
import cn.com.hellowood.dynamicdatasource.modal.Product;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ProductDao {
Product select(@Param("id") long id);
Integer update(Product product);
Integer insert(Product product);
Integer delete(long productId);
List<Product> getAllProduct();
}
- ProductMapper.xml
Запустите проект, посетите в это время
/product/1
вернусьproduct_master
в базе данныхproduct
Все данные в таблице, к которым обращались несколько раз/product
вернется отдельноproduct_slave_alpha
,product_slave_beta
,product_slave_gamma
в базе данныхproduct
Данные в таблице также можно увидеть в журнале переключения источников данных, что свидетельствует о том, что динамическое переключение источников данных эффективно.
Уведомление
В этом приложении, поскольку аспект уровня DAO используется для переключения источника данных, поэтому
@Transactional
Аннотации нельзя добавлять в классы, их можно использовать только в методах.@Trasactional
Аннотированные методы не могут переключать источники данных