本文主要简单实现了影子库
可以理解是一种动态数据源,对于不同请求打到不同的数据源
那么对于影子库的话一般表和表结构和线上库是一致的,为了方便测试数据和真实数据隔离开,采用影子库,测试数据落影子库。
直接上代码,看一版简易实现
首先准备1个数据库服务实例上准备2个数据库
创建一样的表
CREATE TABLE `person` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(25) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
开始通过代码路由到不同的数据源,在person表中新增数据
创建一个新的SpringBoot工程
工程启动入口
package pers.wmx.db;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DynamicDataSourceApplication {
public static void main(String[] args) {
SpringApplication.run(DynamicDataSourceApplication.class, args);
}
}
引入相关依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>pers.wmx</groupId>
<artifactId>db</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>DynamicDataSource</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</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>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
准备配置文件 application.yml
spring:
datasource:
master: # 主数据源
url: jdbc:mysql://localhost:3306/mydb?characterEncoding=utf8&useUnicode=true
username: root
password: wmx123
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
shadow: # 影子数据源
url: jdbc:mysql://localhost:3306/db_shadow?characterEncoding=utf8&useUnicode=true
username: root
password: wmx123
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
准备就绪,开始整我们的代码
把主数据源、影子数据源都通过Spring管理起来
package pers.wmx.db;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
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 com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
/**
* @author wangmingxin03
* Created on 2021-12-08
*/
@Slf4j
@Configuration
public class DynamicDataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return new DruidDataSource();
}
@Bean(name = "shadowDataSource")
@ConfigurationProperties(prefix = "spring.datasource.shadow")
public DataSource shadowDataSource() {
return new DruidDataSource();
}
@Bean
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource, DataSource shadowDataSource) {
Map targetDataSources = new HashMap<>(2);
targetDataSources.put("master", masterDataSource);
targetDataSources.put("shadow", shadowDataSource);
log.info("dataSources:" + targetDataSources);
return new DynamicDataSource(masterDataSource, targetDataSources);
}
}
package pers.wmx.db;
import static pers.wmx.db.AppCustomContext.getDataSource;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author wangmingxin03
* Created on 2021-12-08
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
// 根据数据源名称找到具体需要路由的数据源
return getDataSource();
}
}
我们怎么选择我们的数据源呢?
前端请求来了,我们根据前端请求header携带的标识来判断,带了测试的标识说明就说测试流量,走影子库
我们写个切面来完成上述工作
package pers.wmx.db;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author wangmingxin03
* Created on 2021-12-08
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AppContextAspect {
}
package pers.wmx.db;
import static pers.wmx.db.AppCustomContext.setDataSource;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.util.Strings;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import lombok.extern.slf4j.Slf4j;
/**
* @author wangmingxin03
* Created on 2021-12-08
*/
@Slf4j
@Aspect
@Component
public class DataSourceAspect {
//切面
@Pointcut("@annotation(pers.wmx.db.AppContextAspect)")
public void access() {
}
@Before("access()")
public void doBefore() {
log.info("do before...");
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//获取 Header 流量标记
String header = request.getHeader("trace-context");
if (Strings.isNotEmpty(header) && header.equals("test")) {
// 请求header携带的trace-context是test,表明是测试流量走影子库
setDataSource("shadow");
} else {
setDataSource("master");
}
}
}
上面的 getDataSource()
setDataSource
就是我们存取测试标识的地方,也可以理解成是我们存取是否使用影子库标识的地方
package pers.wmx.db;
import com.alibaba.ttl.TransmittableThreadLocal;
/**
* 上下文信息存储与传递
* 后面可以按需扩展,不仅仅存储动态数据源信息
*
* @author wangmingxin03
* Created on 2021-12-08
*/
public class AppCustomContext {
private static final TransmittableThreadLocal<String> CONTEXT_HOLDER = new TransmittableThreadLocal<>();
public static void setDataSource(String dataSource) { CONTEXT_HOLDER.set(dataSource); }
public static String getDataSource() { return CONTEXT_HOLDER.get(); }
public static void clearDataSource() { CONTEXT_HOLDER.remove(); }
}
后面我们可以在这里扩展存取各种上下文信息
ok,剩下来快速完成Controller, Service,Dao
package pers.wmx.db;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wangmingxin03
* Created on 2021-12-08
*/
@RestController
public class PersonController {
@Autowired
private PersonService personService;
@RequestMapping("/person/insert")
public String insetPerson(@RequestParam String name,
@RequestParam int age) {
personService.insertPerson(name, age);
return "success";
}
}
package pers.wmx.db; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; /** * @author wangmingxin03 * Created on 2021-12-08 */ @Service @Lazy public class PersonService { @Autowired private PersonMapper personMapper; @AppContextAspect public void insertPerson(String name, int age) { Person person = new Person(); person.setName(name); person.setAge(age); personMapper.insert(person); } }
通过注解 @AppContextAspect
做切面拦截,设置流量标识
根据流量标识选择数据源
package pers.wmx.db;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
/**
* @author wangmingxin03
* Created on 2021-12-08
*/
@Mapper
public interface PersonMapper {
@Insert("insert into person(name,age) value(#{name},#{age})")
@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
int insert(Person person);
}
服务跑起来,测试一波
普通请求,落主库
请求header里携带测试流量标识,走影子库
去数据库看一眼
符合预期
说点什么
您将是第一位评论人!