Welcome everyone

影子库实现

java 汪明鑫 422浏览 0评论

本文主要简单实现了影子库

可以理解是一种动态数据源,对于不同请求打到不同的数据源

那么对于影子库的话一般表和表结构和线上库是一致的,为了方便测试数据和真实数据隔离开,采用影子库,测试数据落影子库。

 

直接上代码,看一版简易实现

首先准备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里携带测试流量标识,走影子库

 

 

去数据库看一眼

符合预期

 

 

转载请注明:汪明鑫的个人博客 » 影子库实现

喜欢 (1)

说点什么

您将是第一位评论人!

提醒
avatar
wpDiscuz