Spring Boot系列(三) Spring Boot 之 JDBC

數據源

類型

  • javax.sql.DataSourcejava

  • javax.sql.XADataSourcemysql

  • org.springframework.jdbc.datasource.embedded,EnbeddedDataSourcespring

Spring Boot 中的數據源

單數據源(官方推薦微服務使用單數據源)

  • 數據庫鏈接池sql

    • Apache Commons DBCP數據庫

    • Tomcat DBCPapp

多數據源

實際生產中極可能會出現.ide

事務

  • Spring 中的事務經過PlatformTransactionManagere 管理, 使用 AOP 實現, 具體實現類是 TransactionInterceptor.spring-boot

  • 事務傳播(propagation)與保護點(savepoint): 兩者密切相關.微服務

Spring Boot 使用JDBC

單數據源

  1. 配置:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=xlx
spring.datasource.password=xlx

注意serverTimezone的配置, 亞洲通常配置爲 Hongkong.ui

  1. 訪問類注入DataSource
@Repository
public class ProductRepository {

    @Autowired
    private DataSource dataSource;
    ...
}

多數據源配置

  1. 配置
spring.ds1.name=master
spring.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.ds1.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.ds1.username=xlx
spring.ds1.password=xlx

spring.ds2.name=slave
spring.ds2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.ds2.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.ds2.username=xlx
spring.ds2.password=xlx
  1. 繼承 AbstractRoutingDataSource 抽象類並實現方法determineCurrentLookupKey()

這個類就是須要使用的數據源類型.

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DbTypeContextHolder.get();
    }
}

查看 AbstractRoutingDataSource 的源碼能夠發現, 其定義了一個 Map<Object, Object> targetDataSources; 用來保存多數據源, 而上面重寫的方法返回的就是須要使用的數據源的key.

public enum DBType {
    MASTER,
    SLAVE
}
  1. 定義 DbTypeContextHolder 其中使用一個 ThreadLocal 來保存key, 三個方法都必不可少. 分別用來獲取, 設置, 清除.
@Slf4j
public class DbTypeContextHolder {

    private final static ThreadLocal<DBType> holder = new ThreadLocal<DBType>();

    private final static ThreadLocal<Boolean> alwaysWrite = new ThreadLocal<Boolean>();

    public static DBType get() {
        if (alwaysWrite.get() != null && alwaysWrite.get()) {
            log.info("將強制使用Write");
            return DBType.MASTER;
        }
        log.info("獲取到: "+holder.get().name());
        return holder.get();
    }

    public static void set(DBType dbType) {
        log.info("設置爲: "+dbType.name());
        holder.set(dbType);
    }

    public static void clean() {
        log.info("清除...");
        holder.remove();
    }

    public static void setAlwaysWrite() {
        log.info("設置強制Write");
        alwaysWrite.set(true);
    }

    public static void cleanAlwaysWrite() {
        log.info("清除強制Write");
        clean();
        alwaysWrite.remove();
    }

}
  1. 定義數據源配置類 MultiDataSourceConfiguration
@Configuration
public class MultiDataSourceConfiguration {

    @Autowired
    Environment environment;

    @Bean
    public DataSource getDynamicDataSource(){
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object,Object> map = new HashMap<>();

        for (int i = 1; i < 3 ; i++) {
            String name = environment.getProperty("spring.ds"+String.valueOf(i)+".name");
            if (name==null) break;
            DataSource dataSource = DataSourceBuilder.create()
                    .driverClassName(environment.getProperty("spring.ds"+String.valueOf(i)+".driver-class-name"))
                    .username(environment.getProperty("spring.ds"+String.valueOf(i)+".username"))
                    .url(environment.getProperty("spring.ds"+String.valueOf(i)+".url"))
                    .password(environment.getProperty("spring.ds"+String.valueOf(i)+".password"))
                    .build();
            if (name.trim().toUpperCase().equals("MASTER")){
                dynamicDataSource.setDefaultTargetDataSource(dataSource);
                map.put(DBType.MASTER,dataSource);
            }else{
                map.put(DBType.SLAVE,dataSource);
            }
        }

        dynamicDataSource.setTargetDataSources(map);
        return dynamicDataSource;
    }
}
  1. 檢查是否可用
public Boolean save(Product product) throws SQLException {
    System.out.println("save product : "+product);
    DbTypeContextHolder.set(DBType.MASTER);
    System.out.println(dataSource.getConnection());
    DbTypeContextHolder.clean();

    DbTypeContextHolder.set(DBType.SLAVE);
    System.out.println(dataSource.getConnection());
    DbTypeContextHolder.clean();
    return true;
}

結果以下,說明是兩個不一樣的數據源:

HikariProxyConnection@1667860231 wrapping com.mysql.cj.jdbc.ConnectionImpl@78794b01
HikariProxyConnection@556437990 wrapping com.mysql.cj.jdbc.ConnectionImpl@314c0916
  1. 自動切換

能夠定義切面來自動切換數據源. 好比普通的讀寫分離.
讀寫分離是經過定義在倉儲層方法上的規則來實現, 同時也定義了在服務層必須使用寫庫來讀取數據的方式.

除此以外, 若是使用 @Transactional , 由於其也是經過AOP方式實現, 因此與自動切換的AOP存在順序關係, 爲了能在 @Transactional 事務前設置好數據源, 須要增長 @Order(0) .

@Aspect
@Component
@Order(0)
public class DataSourceAspect {

    // 必須使用寫庫的
    @Pointcut("execution(* com.xlx.product.repository.repo..*.save*(..)) || execution(* com.xlx.product.repository.repo..*.insert*(..)) || execution(* com.xlx.product.repository.repo..*.update*(..))")
    public void write(){}

    @Before("write()")
    public void beforeWrite(JoinPoint joinPoint){
        System.out.println("beforeWrite()"+joinPoint.getSignature().getName());
        DbTypeContextHolder.set(DBType.MASTER);
    }

    @After("write()")
    public void afterWrite(JoinPoint joinPoint){
        DbTypeContextHolder.clean();
    }

    // 必須使用讀庫的
    @Pointcut("execution(* com.xlx.product.repository.repo..*.get*(..))|| execution(* com.xlx.product.repository.repo..*.select*(..))||execution(* com.xlx.product.repository.repo..*.count*(..))")
    public void read(){}

    @Before("read()")
    public void beforeRead(JoinPoint joinPoint){
        DbTypeContextHolder.set(DBType.SLAVE);
    }

    @After("read()")
    public void afterRead(JoinPoint joinPoint){
        DbTypeContextHolder.clean();
    }

    // 服務層有註解的方法特殊處理
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)" +
            "||@annotation(com.xlx.product.repository.annotation.DBTypeAnnotation)")
    public void readWrite(){}

    @Before("readWrite()")
    public void before(JoinPoint joinPoint){
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        if(methodSignature.getMethod().isAnnotationPresent(Transactional.class)) DbTypeContextHolder.set(DBType.MASTER);
        if(methodSignature.getMethod().isAnnotationPresent(DBTypeAnnotation.class)) DbTypeContextHolder.setAlwaysWrite();
    }

    @After("readWrite()")
    public void after(JoinPoint joinPoint){
        DbTypeContextHolder.clean();
        DbTypeContextHolder.cleanAlwaysWrite();
    }

}
/**
 * 放置在方法上的註解, 用來指定使用的數據源類型, 默認使用主數據源
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DBTypeAnnotation {

    @AliasFor("values")
    DBType dbType() default DBType.MASTER;

    @AliasFor("dbType")
    DBType values() default DBType.MASTER;
}
  1. service 和 controller
@Service
@Slf4j
public class ProductService {

    @Autowired
    ProductRepository repository;

    public String findAndSave(){
        log.info("findAndSave() 方法開始.....");
        String rs = testFunc();
        log.info("findAndSave() 方法結束.....");
        return rs;
    }

    @DBTypeAnnotation
    public String findAndSaveWithWrite(){
        log.info("findAndSaveWithWrite() 方法開始.....");
        String rs = testFunc();
        log.info("findAndSaveWithWrite() 方法結束.....");
        return rs;
    }

    private String testFunc() {
        List<Product> productList = repository.selectAll();
        repository.saveProduct(productList.get(0));
        productList = repository.selectAll();
        repository.saveProduct(productList.get(0));
        return "ok";
    }

}
@RestController
public class ProductController {

    @Autowired
    ProductService productService;

    @GetMapping("/product/findAndSave")
    public String findAndSave()throws SQLException {
        return productService.findAndSave();
    }

    @GetMapping("/product/findAndSaveWithWrite")
    public String findAndSaveWithWrite()throws SQLException {
        return productService.findAndSaveWithWrite();
    }
}
相關文章
相關標籤/搜索