springboot整合Mybatis、事務、多資料來源、分散式事務
springboot整合Mybatis、事務、多資料來源
文章目錄
一. 整合Mybatis
第一步:匯入包
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
第二步:如果需要使用mapper配置檔案的,可以在application.properties中新增配置
mybatis.mapper-locations=classpath:mapper/*.xml
第三步:在配置類中新增配置
@MapperScan("com.qianfeng.demo.dao") // 掃描mybatis的介面類
@SpringBootApplication
public class Demo31Application {
public static void main(String[] args) {
SpringApplication.run(Demo31Application.class, args);
}
}
第四步:編寫介面並編寫註解配置(或編寫對應的Mapper.xml)
@Repository
public interface StudentDAO {
@Select({"SELECT id, name, sex from student_info"})
public List<Student> findAll();
}
二. 事務
2.1 回顧事務的特徵
2.1.1 什麼是事務(Transaction)
是併發控制的單元,是使用者定義的一個操作序列。這些操作要麼都做,要麼都不做,是一個不可分割的工作單位。通過事務,sql 能將邏輯相關的一組操作繫結在一起,以便伺服器 保持資料的完整性。事務通常是以begin transaction開始,以commit或rollback結束。Commint表示提交,即提交事務的所有操作。具體地說就是將事務中所有對資料的更新寫回到磁碟上的物理資料庫中去,事務正常結束。Rollback表示回滾,即在事務執行的過程中發生了某種故障,事務不能繼續進行,系統將事務中對資料庫的所有已完成的操作全部撤消,滾回到事務開始的狀態。
設想網上購物的一次交易,其付款過程至少包括以下幾步資料庫操作:
1)更新客戶所購商品的庫存資訊
2)儲存客戶付款資訊–可能包括與銀行系統的互動
3)生成訂單並且儲存到資料庫中
4)更新使用者相關資訊,例如購物數量等等
正常的情況下,這些操作將順利進行,最終交易成功,與交易相關的所有資料庫資訊也成功地更新。但是,如果在這一系列過程中任何一個環節出了差錯,例如在更新商品庫存資訊時發生異常、該顧客銀行帳戶存款不足等,都將導致交易失敗。一旦交易失敗,資料庫中所有資訊都必須保持交易前的狀態不變,比如最後一步更新使用者資訊時失敗而導致交易失敗,那麼必須保證這筆失敗的交易不影響資料庫的狀態–庫存資訊沒有被更新、使用者也沒有付款,訂單也沒有生成。否則,資料庫的資訊將會一片混亂而不可預測。
資料庫事務正是用來保證這種情況下交易的平穩性和可預測性的技術
2.1.2 為什麼要使用事務?
- 為了提高效能
- 為了保持業務流程的完整性
2.1.3 事務的特性
ACID
- 原子性(atomicity)
事務是資料庫的邏輯工作單位,而且是必須是原子工作單位,對於其資料修改,要麼全部執行,要麼全部不執行。
- 一致性(consistency)
事務在完成時,必須是所有的資料都保持一致狀態。在相關資料庫中,所有規則都必須應用於事務的修改,以保持所有資料的完整性。
- 隔離性(isolation)
一個事務的執行不能被其他事務所影響。企業級的資料庫每一秒鐘都可能應付成千上萬的併發訪問,因而帶來了併發控制的問題。由資料庫理論可知,由於併發訪問,在不可預料的時刻可能引發如下幾個可以預料的問題:
- 永續性(durability)
一個事務一旦提交,事物的操作便永久性的儲存在DB中。即使此時再執行回滾操作也不能撤消所做的更改
2.1.4 事務的併發問題
- 髒讀(Dirty Read)
一個事務讀取到了另一個事務未提交的資料操作結果。這是相當危險的,因為很可能所有的操作都被回滾。
- 不可重複讀(虛讀)(NonRepeatable Read)
一個事務對同一行資料重複讀取兩次,但是卻得到了不同的結果。例如事務T1讀取某一資料後,事務T2對其做了修改,當事務T1再次讀該資料時得到與前一次不同的值。
- 幻讀(Phantom Read)
事務在操作過程中進行兩次查詢,第二次查詢的結果包含了第一次查詢中未出現的資料或者缺少了第一次查詢中出現的資料,這是因為在兩次查詢過程中有另外一個事務插入資料造成的
2.1.5 事務的隔離級別
- 1- 讀未提交
Read uncommitted:最低級別,以上情況均無法保證。
- 2- 讀已提交
Read committed:可避免髒讀情況發生。(Oracle預設)
- 4- 可重複讀
Repeatable read:可避免髒讀、不可重複讀情況的發生。不可以避免虛讀。(MySQl預設)
- 8- 序列化讀
Serializable:事務只能一個一個執行,避免了髒讀、不可重複讀、幻讀。執行效率慢,使用時慎重.
2.1.6 事務的傳播行為
事務傳播行為用來描述由某一個事務傳播行為修飾的方法被巢狀進另一個方法的時事務如何傳播。
注意:Spring在TransactionDefinition介面中規定了7種類型的事務傳播行為。事務傳播行為是Spring框架獨有的事務增強特性,他不屬於的事務實際提供方資料庫行為。這是Spring為我們提供的強大的工具箱,使用事務傳播行可以為我們的開發工作提供許多便利。
Spring中七種事務傳播行為:
事務傳播行為型別 | 說明 |
---|---|
PROPAGATION_REQUIRED | 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。 |
PROPAGATION_SUPPORTS | 支援當前事務,如果當前沒有事務,就以非事務方式執行。 |
PROPAGATION_MANDATORY | 使用當前的事務,如果當前沒有事務,就丟擲異常。 |
PROPAGATION_REQUIRES_NEW | 新建事務,如果當前存在事務,把當前事務掛起。 |
PROPAGATION_NOT_SUPPORTED | 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 |
PROPAGATION_NEVER | 以非事務方式執行,如果當前存在事務,則丟擲異常。 |
PROPAGATION_NESTED | 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。 |
2.2 spring整合MyBatis的事務配置(宣告式事務)
2.2.1 使用配置檔案方式:applicationContext.xml
<!-- 配置事務管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref bean="dataSource" />
</property>
</bean>
<!-- 配置通知(配置事務執行的方法定義的名稱規則) -->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="del*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="*" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 配置AOP的規則 -->
<aop:config>
<!-- 配置切入點 -->
<aop:pointcut id="pc" expression="execution(* com.qianfeng.demo.service.*.*(..))" />
<aop:advisor pointcut-ref="pc" advice-ref="myAdvice" />
</aop:config>
注意:使用此配置要求service中的方法名稱必須按照規則編寫。
2.2.2 使用註解的方式
第一步:在配置檔案中新增:
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
第二步:在類中添加註解即可:
@Service
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public class CustomerService {
@Resource
private CustomerDAO customerDAO;
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public List<Customer> findAll(){
return customerDAO.findAll();
}
}
2.3 在springboot中使用事務
springboot由於可以自動配置,所以幾乎不需要任何配置就可以使用註解的方式實現事務。
@Service
public class CustomerService {
@Resource
private CustomerDAO customerDAO;
public List<Customer> findAll(){
return customerDAO.findAll();
}
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void transferMoney(Integer fromId, Integer toId, Double money) throws Exception {
customerDAO.increaseMoney(toId, money);
Customer customer = customerDAO.findById(fromId);
if (customer != null && customer.getMoney() >= money) {
customerDAO.decreaseMoney(fromId, money);
}else {
throw new Exception("餘額不足");
}
}
}
三. 在springboot中整合mybatis使用多資料來源
3.1 多資料來源的配置
springboot中可以使用多個數據源,但是需要對多個數據源進行配置,最簡單的辦法就是分別配置:
第一步:在application.properties
中配置各個資料來源的引數
#第一個資料來源
spring.datasource.bank1.username=root
spring.datasource.bank1.password=root
spring.datasource.bank1.url=jdbc:mysql://localhost:3306/bank1?useUnicode=true&characterEncoding=utf-8
spring.datasource.bank1.driver-class-name=com.mysql.jdbc.Driver
bank1.mybatis.mapper-locations=classpath:mapper/bank1/*.xml
#第二個資料來源
spring.datasource.bank2.username=root
spring.datasource.bank2.password=root
spring.datasource.bank2.url=jdbc:mysql://localhost:3306/bank2?useUnicode=true&characterEncoding=utf-8
spring.datasource.bank2.driver-class-name=com.mysql.jdbc.Driver
bank2.mybatis.mapper-locations=classpath:mapper/bank2/*.xml
第二步:編寫對應的配置類:(第一個資料來源的配置)
@Configuration
// 掃描的包路徑
@MapperScan(basePackages = "com.qianfeng.demo.dao.bank1",
sqlSessionTemplateRef = "bank1SqlSessionTemplate") // 引用的template的name
public class Bank1DataSourceConfig {
// 引用application.properties中的路徑
@Value("${bank1.mybatis.mapper-locations}")
private String mapperLocation;
// 配置dataSource
@Bean(name = "bank1DataSource")
@Primary // 配置為預設值
@ConfigurationProperties(prefix = "spring.datasource.bank1") // application.properties中資料來源的字首
public DataSource bank1DataSource(){
return DataSourceBuilder.create().build();
}
@Primary // 配置為預設值
@Bean(name = "bank1SqlSessionFactory")
public SqlSessionFactory bank1SqlSessionFactory(@Qualifier("bank1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(mapperLocation));
return sqlSessionFactoryBean.getObject();
}
@Bean(name = "bank1TransactionManager")
@Primary // 配置為預設值
public DataSourceTransactionManager bank1TransactionManager(@Qualifier("bank1DataSource") DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "bank1SqlSessionTemplate")
@Primary // 配置為預設值
public SqlSessionTemplate bank1SqlSessionTemplate(@Qualifier("bank1SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
第二個資料來源的配置:
注意:幾乎和第一個資料來源的配置幾乎一樣,但是一定不要配置@Primary註解,因為不可能有兩個預設值
@Configuration
@MapperScan(basePackages = "com.qianfeng.demo.dao.bank2",
sqlSessionTemplateRef = "bank2SqlSessionTemplate")
public class Bank2DataSourceConfig {
@Value("${bank2.mybatis.mapper-locations}")
private String mapperLocation;
@Bean(name = "bank2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.bank2")
public DataSource bank2DataSource(){
return DataSourceBuilder.create().build();
}
@Bean(name = "bank2SqlSessionFactory")
public SqlSessionFactory bank2SqlSessionFactory(@Qualifier("bank2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(mapperLocation));
return sqlSessionFactoryBean.getObject();
}
@Bean(name = "bank2TransactionManager")
public DataSourceTransactionManager bank2TransactionManager(@Qualifier("bank2DataSource") DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "bank2SqlSessionTemplate")
public SqlSessionTemplate bank2SqlSessionTemplate(@Qualifier("bank2SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
注意:DAO中名稱的唯一性,避免系統自動裝配的時候找不到唯一的名稱。
3.2 多資料來源的事務管理
由於多資料來源中的資料是跨資料庫的,單資料庫那種事務管理是無效的,這裡需要使用JTA去完成(這裡使用atomikos)。
第一步:匯入atomikos的包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
第二步:在application.properties
中編寫atomikos的多資料來源的配置(這裡只是簡單配置,更多引數配置請參考對應的文件)
spring.jta.enabled=true
spring.datasource.bank1.xa-properties.url=jdbc:mysql://localhost:3306/bank1?useUnicode=true&characterEncoding=utf-8
spring.datasource.bank1.xa-properties.user=root
spring.datasource.bank1.xa-properties.password=root
spring.datasource.bank1.xa-data-source-class-name=com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
spring.datasource.bank1.unique-resource-name=bank1DataSource
bank1.mybatis.mapper-locations=classpath:mapper/bank1/*.xml
spring.datasource.bank2.xa-properties.url=jdbc:mysql://localhost:3306/bank2?useUnicode=true&characterEncoding=utf-8
spring.datasource.bank2.xa-properties.user=root
spring.datasource.bank2.xa-properties.password=root
spring.datasource.bank2.xa-data-source-class-name=com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
spring.datasource.bank2.unique-resource-name=bank2DataSource
bank2.mybatis.mapper-locations=classpath:mapper/bank2/*.xml
第三步:修改配置類中的程式碼,將原來的事務管理的程式碼註釋掉,並將資料來源換成atomikos的資料來源。
@Configuration
@MapperScan(basePackages = "com.qianfeng.demo.dao.bank1",
sqlSessionTemplateRef = "bank1SqlSessionTemplate")
public class Bank1DataSourceConfig {
@Value("${bank1.mybatis.mapper-locations}")
private String mapperLocation;
@Bean(name = "bank1DataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.bank1")
public DataSource bank1DataSource(){
// return DataSourceBuilder.create().build();
return new AtomikosDataSourceBean();
}
@Primary
@Bean(name = "bank1SqlSessionFactory")
public SqlSessionFactory bank1SqlSessionFactory(@Qualifier("bank1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(mapperLocation));
return sqlSessionFactoryBean.getObject();
}
// @Bean(name = "bank1TransactionManager")
// @Primary
// public DataSourceTransactionManager bank1TransactionManager(@Qualifier("bank1DataSource") DataSource dataSource){
// return new DataSourceTransactionManager(dataSource);
// }
@Bean(name = "bank1SqlSessionTemplate")
@Primary
public SqlSessionTemplate bank1SqlSessionTemplate(@Qualifier("bank1SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}