Spring Boot系列(三) Spring Boot 之 JDBC
数据源
类型
-
javax.sql.DataSource
-
javax.sql.XADataSource
-
org.springframework.jdbc.datasource.embedded,EnbeddedDataSource
Spring Boot 中的数据源
单数据源(官方推荐微服务使用单数据源)
-
数据库连接池
-
Apache Commons DBCP
-
Tomcat DBCP
-
多数据源
实际生产中很可能会出现.
事务
-
Spring 中的事务通过
PlatformTransactionManagere
管理, 使用 AOP 实现, 具体实现类是TransactionInterceptor
. -
事务传播(propagation)与保护点(savepoint): 二者密切相关.
Spring Boot 使用JDBC
单数据源
- 配置:
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
.
- 访问类注入
DataSource
@Repository
public class ProductRepository {
@Autowired
private DataSource dataSource;
...
}
多数据源配置
- 配置
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
- 继承
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
}
- 定义
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();
}
}
- 定义数据源配置类
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;
}
}
- 检查是否可用
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
- 自动切换
可以定义切面来自动切换数据源. 比如普通的读写分离.
读写分离是通过定义在仓储层方法上的规则来实现, 同时也定义了在服务层必须使用写库来读取数据的方式.
除此之外, 如果使用 @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;
}
- 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();
}
}