内容

  • 数据源DataSource
  • 事务Transation
  • JDBC4.0(JSR-221)
  • 问题及回答

数据源

数据源是数据库连接的来源,通过DataSource接口获取。

  • 类型
    • 通用型数据源
      • javax.sql.DataSource
    • 分布式数据源
      • javax.sql.XADataSource
    • 嵌入式数据源
      • org.springframework.jdbc.datasource.embedded.EmbeddedDatasource

Spring Boot实际使用场景

  • 在Spring Boot 2.0.0
    • 如果采用Spring WebMVC作为Web服务,默认情况下,使用嵌入式Tomcat。
    • 如果采用Spring WebFlux,默认情况下,使用嵌入式Netty Web Server。
  • 传统的Servlet采用HttpServletRequest、HttpServletResponse
  • WebFlux采用:ServletRequest、ServletResponse
    • 不再限制于Servlet容器,可以选择自定义实现,比如Netty Web Server
单数据源的场景

数据库连接池技术

Apache Commons DBCP

  • commons-dbcp2
    • 依赖commons-pool2
  • commons-dbcp
    • 依赖commons-pool

Tomcat DBCP

代码示例
  • 创建项目
    • 版本
      • 2.0.0M7
    • 依赖
      • JDBC、MySQL、Reactive Web
  • 启动项目
    • 会报错:Cannot determine embedded database driver class for database type NONE
  • 如果没有配置数据源,就将pom文件下jdbc依赖注释掉。
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

报错源码

  • 调用流程

org.springframework.boot.autoconfigure.jdbc包下

DataSourceConfiguration.Hikari#dataSource -> DataSourceConfiguration#createDataSource-> DataSourceProperties#initializeDataSourceBuilder ->

DataSourceProperties#determineDriverClassName

  • 报错位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String determineDriverClassName() {
if (StringUtils.hasText(this.driverClassName)) {
Assert.state(driverClassIsLoadable(),
() -> "Cannot load driver class: " + this.driverClassName);
return this.driverClassName;
}
String driverClassName = null;

if (StringUtils.hasText(this.url)) {
driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
}

if (!StringUtils.hasText(driverClassName)) {
driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
}
//这里报错
if (!StringUtils.hasText(driverClassName)) {
throw new DataSourceBeanCreationException(this.embeddedDatabaseConnection,
this.environment, "driver class");
}
return driverClassName;
}
配置数据源
  • application.properties添加内容

  • 1
    2
    3
    4
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url= jdbc://localhost:3306/test
    spring.datasource.username= root
    spring.datasource.password= root
  • 再次启动

    • 控制台输出Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
    • 代表已经配置好了
1
2
3
4
//@ConfigurationProperties 对应 application.properties
//prefix 就是内容
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties
多数据源的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.bai.springbootjdbc.config;

import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;

/**
* 多数据源配置
* 方法的名称作为Bean的名称
*/
@Configuration
public class MultipleDataSourceConfiguration {
/**
* spring.datasource.driver-class-name=com.mysql.jdbc.Driver
* spring.datasource.url= jdbc://localhost:3306/test
* spring.datasource.username= root
* spring.datasource.password= root
*
* @return
*/

@Bean
@Primary
public DataSource masterDataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
DataSource dataSource = dataSourceBuilder.driverClassName("com.mysql.jdbc.Driver")
.url("jdbc://localhost:3306/test")
.username("root")
.password("root")
.build();
return dataSource;
}

@Bean
public DataSource slaveDataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
DataSource dataSource = dataSourceBuilder.driverClassName("com.mysql.jdbc.Driver")
.url("jdbc://localhost:3306/test2")
.username("root")
.password("root")
.build();
return dataSource;
}
}

考虑到容灾、负载均衡的情况,更多的是通过MySQL的代理来做。

配置多数据源不能通过application.properties,是因为无法区分Bean的名称。

事务Transaction

事务用于提供数据完整性,并在并发访问下确保数据视图的一致性。

  • 概念
    • 自动提交模式Auto-commitmode
    • 事务隔离级别Transaction isolation levels
    • 保护点Savepoints

自动提交模式和手动提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public boolean save(User user) {
boolean result = false;
Connection connection = null;
try {
connection = this.masterDataSource.getConnection();
//手动提交
//connection.setAutoCommit(false);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO users(name) VALUES (?);");
preparedStatement.setString(1, user.getName());
result = preparedStatement.executeUpdate() > 0 ? true : false;
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
//手动提交
//connection.commit();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
};
}
return result;
}

Annotation 驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
@Transactional
public boolean save(User user) {
boolean result = false;
result=jdbcTemplate.execute("INSERT INTO users(name) VALUES (?);", new PreparedStatementCallback<Boolean>() {
@Nullable
@Override
public Boolean doInPreparedStatement(PreparedStatement preparedStatement) throws SQLException, DataAccessException {
preparedStatement.setString(1, user.getName());
return preparedStatement.executeUpdate() > 0;
}
});
return result;
}

事务隔离级别Transaction isolation levels

脏读:脏读又称无效数据读出。一个事务读取另外一个事务还没有提交的数据叫脏读。

不可重复读:不可重复读的重点是修改 。不可重复读是指在同一个事务内,两个相同的查询返回了不同的结果。

幻读:幻读的重点在于新增或者删除 。统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样。这就叫幻读。

  • java.sql.Connection中四个属性
    • TRANSACTION_READ_UNCOMMITTED
      • 读取未提交的
      • 脏读、不可重复读、幻读会发生
    • TRANSACTION_READ_COMMITTED
      • 读取已提交的
      • 解决脏读,但不可重复读、幻读会发生
    • TRANSACTION_REPEATABLE_READ
      • 不可重复读
      • 解决脏读和不可重复读,幻读会发生
    • TRANSACTION_SERIALIZABLE
      • 事务
      • 解决脏读、不可重复读和幻读
  • 脏读、不可重复读、幻读的级别高低是:脏读 < 不可重复读 < 幻读。
  • 级别越高性能越差

Spring JDBC Transaction实现重写了JDBC API:

org.springframework.transaction.annotation.Isolation ->org.springframework.transaction.TransactionDefinition ->

java.sql.Connection

通过AOP的方式代理了Connection,自动提交模式关闭,经过一系列操作后,方法执行完毕,再提交事务。

@Transaction执行代理-TransactionInterceptor

org.springframework.transaction.annotation.Transactional

  • 可以控制rollback的异常粒度
    • rollbackFor方法以及noRollbackFor方法
  • 可以执行事务管理器
    • transactionManager

API驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Transactional
public boolean save(User user) {
boolean result = false;
DefaultTransactionAttribute defaultTransactionAttribute=new DefaultTransactionAttribute();
//开始事务
TransactionStatus transactionStatus=platformTransactionManager.getTransaction(defaultTransactionAttribute);
result=jdbcTemplate.execute("INSERT INTO users(name) VALUES (?);", new PreparedStatementCallback<Boolean>() {

@Nullable
@Override
public Boolean doInPreparedStatement(PreparedStatement preparedStatement) throws SQLException, DataAccessException {
preparedStatement.setString(1, user.getName());
return preparedStatement.executeUpdate() > 0;
}
});

platformTransactionManager.commit(transactionStatus);
return result;
}

场景选择

是否事务嵌套,这就涉及到了事务传播

打了@Transactional标签的save方法调用没有打@Transactionalsave2方法,单独调用save2是没有事务的

  • org.springframework.transaction.annotation.Propagation
    • REQUIRED
      • 默认
      • 无:新建;有:加入当前事务
    • SUPPORTS
      • 无:按非事务执行;有:使用当前事务
    • MANDATORY
      • 无:报错;有:使用当前事务
    • REQUIRES_NEW
      • 无:新建一个事务;有:当前事务挂起
    • NOT_SUPPORTED
      • 无:按照非事务执行;有:当前事务挂起
    • NEVER
      • 无:非事务执行,有:报错
    • NESTED
      • 无:新建事务;有:当前事务嵌套其它事务

保护点Savepoints

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public boolean jdbcSave(User user) {
boolean result = false;
Connection connection = null;
try {
connection = this.masterDataSource.getConnection();
connection.setAutoCommit(false);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO users(name) VALUES (?);");
preparedStatement.setString(1, user.getName());
result = preparedStatement.executeUpdate() > 0 ? true : false;
Savepoint savepoint = connection.setSavepoint("T1");
try {
transactionalSave(user);
} catch (Exception ex) {
connection.rollback(savepoint);
}
connection.commit();
connection.releaseSavepoint(savepoint);
preparedStatement.close();

} catch (SQLException e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.commit();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return result;
}

@Transactional
public boolean transactionalSave(User user) {
boolean result = false;
// 故意写错列名
result = jdbcTemplate.execute("INSERT INTO logs(name1) VALUES (?);", new PreparedStatementCallback<Boolean>() {

@Nullable
@Override
public Boolean doInPreparedStatement(PreparedStatement preparedStatement) throws SQLException, DataAccessException {
preparedStatement.setString(1, "rollback");
return preparedStatement.executeUpdate() > 0;
}
});

return result;
}

jdbcSave调用transactionalSave,在transactionalSave内部故意写错列名导致发生错误,最后结果是jdbcSave可以正常提交保存成功,transactionalSave被回滚,数据不会插入到数据库中。

问题&解答

Q:用reactive web,原来MVC的好多东西都不能用了?

A:不是,Reactive Web还是能够兼容Spring Web Mvc

Q:SP1调用SP2,如果SP2是注解新开一个事务的话,那么和嵌套事务有什么区别?

A:NESTED是根据JDBC驱动来实现的,不是Spring实现的,不一定所有数据库都支持。REQUIRES_NEW有独立的事务环境,NESTED是共享的(commit和rollback)

Q:开个线程池事务控制API方式?比如写的Excutor.fixExcutor(5)

A:TransactionSynchronizationManager使用大量的ThreadLocal来实现的。

Q:假设一个service方法打了@Transation,在这个方法中还有其它service的某个方法,这个方法没有加@Transation,那么如果内部方法报错,会回滚吗?

A:会。当前可以过滤掉一些无关紧要的异常noRollbackfor()。

Q:Spring 分布式事务生产环境实现方式有哪些?

A:Distributed Transactions with JTA