事務是什麼?html
事務的特性java
併發事務的問題mysql
事務的隔離級別git
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
複製代碼
事務傳播機制,也就是在事務在多個方法的調用中是如何傳遞的;如:兩個service的方法都是事務操做,一個事務方法調用另外一個事務方法如何進行處理?github
public interface TransactionDefinition {
// 事務的傳播機制
// 若是調用方有事務則使用調用方的事務,若是不存在事務則建立一個事務
int PROPAGATION_REQUIRED = 0;
// 跟隨調用方,若是調用方有 那就用,調用方沒有那就不用
int PROPAGATION_SUPPORTS = 1;
// 調用方的方法必須運行在一個事務中,不存在事務則拋出異常
int PROPAGATION_MANDATORY = 2;
// 無論調用方是否有事務執行,本身都要起一個新事務。把原先的事務掛起,這個只在jta的事務管理器中起做用(事務是不支持嵌套的)
int PROPAGATION_REQUIRES_NEW = 3;
// 即便調用方有事務,我也要在非事務中執行,把原先的事務掛起
int PROPAGATION_NOT_SUPPORTED = 4;
// 毫不在事務裏面執行
int PROPAGATION_NEVER = 5;
// 嵌套事務(實際是不支持的,是利用存盤點來實現,把調用方以前執行的存盤點,而後相似於再開啓一個事務執行,執行完畢後恢復存盤點,繼續執行。JDBC3.0以上支持)
int PROPAGATION_NESTED = 6;
// 如下定義的是事務的隔離機制
// 根據數據庫的默認的隔離機制而定
int ISOLATION_DEFAULT = -1;
// 讀未提交
int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;
// 讀已提交
int ISOLATION_READ_COMMITTED = 2; // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;
// 可重複讀
int ISOLATION_REPEATABLE_READ = 4; // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;
// 串行化
int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;
// 默認超時時間,以數據庫設定爲準
int TIMEOUT_DEFAULT = -1;
// 獲取事務的傳播屬性
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
// 獲取事務的隔離級別
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
// 獲取事務超時時間
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
// 事務是否只讀。
default boolean isReadOnly() {
return false;
}
// 獲取事務的名稱
@Nullable
default String getName() {
return null;
}
// 返回一個默認事務定義
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}
}
複製代碼
// Savepoiont就是在Nested這種傳播機制中提供保存點機制來實現嵌套事務,出錯的時候能夠選擇恢復到保存點
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
// 是否有保存點
boolean hasSavepoint();
@Override
void flush();
}
複製代碼
public interface TransactionExecution {
// 是不是一個新事務
boolean isNewTransaction();
// 設置事務回滾
void setRollbackOnly();
// 事務是否回滾
boolean isRollbackOnly();
// 獲取事務是否完成
boolean isCompleted();
}
複製代碼
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDAO accountDAO;
@Autowired
PlatformTransactionManager transactionManager;
/** * 聲明式事務 * propagation = Propagation.REQUIRED (默認值就是REQUIRED) 若是調用方有事務就直接使用調用方的事務,若是沒有就新建一個事務 * transactionManager = "transactionManager" 也是默認值 * isolation= Isolation.DEFAULT 隔離級別 * 還有timeout等參數 可自行查看Transactional的源碼 裏面都有說明 * @param sourceAccountId 源帳戶 * @param targetAccountId 目標帳戶 * @param amount 金額 * @return 操做結果信息 */
@Override
@Transactional(transactionManager = "transactionManager",propagation = Propagation.REQUIRED, rollbackFor = Exception.class, isolation= Isolation.DEFAULT)
public String transferAnnotation(Long sourceAccountId, Long targetAccountId, BigDecimal amount) {
AccountDO sourceAccountDO = accountDAO.selectByPrimaryKey(sourceAccountId);
AccountDO targetAccountDO = accountDAO.selectByPrimaryKey(targetAccountId);
if (null == sourceAccountDO || null == targetAccountDO) {
return "轉入或者轉出帳戶不存在";
}
if (sourceAccountDO.getBalance().compareTo(amount) < 0) {
return "轉出帳戶餘額不足";
}
sourceAccountDO.setBalance(sourceAccountDO.getBalance().subtract(amount));
accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
// error("annotation error!");
targetAccountDO.setBalance(targetAccountDO.getBalance().add(amount));
accountDAO.updateByPrimaryKeySelective(targetAccountDO);
return "轉帳成功!";
}
@Override
public String transferCode(Long sourceAccountId, Long targetAccountId, BigDecimal amount) {
TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
// 獲取事務 開始業務執行
TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
try {
AccountDO targetAccountDO = accountDAO.selectByPrimaryKey(targetAccountId);
AccountDO sourceAccountDO = accountDAO.selectByPrimaryKey(sourceAccountId);
if (null == sourceAccountDO || null == targetAccountDO) {
return "轉入或者轉出帳戶不存在";
}
error("code error");
if (sourceAccountDO.getBalance().compareTo(amount) < 0) {
return "轉出帳戶餘額不足";
}
sourceAccountDO.setBalance(sourceAccountDO.getBalance().subtract(amount));
targetAccountDO.setBalance(targetAccountDO.getBalance().add(amount));
accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
// 提交事務
transactionManager.commit(transaction);
return "轉帳成功!";
} catch (Exception e) {
log.error("轉帳發生錯誤,開始回滾,source: {}, target: {}, amount: {}, errMsg: {}",
sourceAccountId, targetAccountId, amount, e.getMessage());
// 報錯回滾
transactionManager.rollback(transaction);
}
return "轉帳失敗";
}
@Override
public List<AccountDO> listAll() {
return accountDAO.selectAll();
}
private static void error(String msg) {
throw new RuntimeException(msg);
}
}
複製代碼
使用註解式事務,Spring會利用代理實現一個代理類,web
而後從上下文中獲得事務管理器,開啓一個事務後執行業務代碼,spring
若是知足你設置的回滾異常條件,就執行rollbacksql
咱們調用的時候是直接調用的Service的方法數據庫
可是Spring在實現的時候,實際是經過AOP Proxy(AOP 代理服務)來調用Transaction Advisor(作事務管理的)而後來處理調用註解式事務的service方法apache
o.s.web.servlet.DispatcherServlet : GET "/api/account/transfer/annotation?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to io.ilss.transaction.onedatasource.web.AccountController#transferAnnotation(Long, Long, String)
o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [io.ilss.transaction.onedatasource.service.impl.AccountServiceImpl.transferAnnotation]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'transactionManager',-java.lang.Exception
o.s.j.d.DataSourceTransactionManager : Acquired Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] for JDBC transaction
o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] to manual commit
org.mybatis.spring.SqlSessionUtils : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
o.m.s.t.SpringManagedTransaction : JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] will be managed by Spring
i.i.t.o.d.AccountDAO.selectByPrimaryKey : ==> Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey : ==> Parameters: 1(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey : <== Total: 1
org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.AccountDAO.selectByPrimaryKey : ==> Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey : ==> Parameters: 2(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey : <== Total: 1
org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective : ==> Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective : ==> Parameters: 小一(String), xiaoyi(String), 123456(String), 877.00(BigDecimal), 2020-01-09T17:04:28(LocalDateTime), 2020-01-09T17:44:33(LocalDateTime), 1(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective : <== Updates: 1
org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective : ==> Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective : ==> Parameters: 小二(String), xiaoer(String), 123456(String), 223.00(BigDecimal), 2020-01-09T17:04:40(LocalDateTime), 2020-01-09T17:04:40(LocalDateTime), 2(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective : <== Updates: 1
org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@185f0a96]
o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] after transaction
m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
m.m.a.RequestResponseBodyMethodProcessor : Writing ["轉帳成功!"]
o.s.web.servlet.DispatcherServlet : Completed 200 OK
複製代碼
- GET 請求到 /api/account/transfer/annotation接口 而後調用service方法
- 根據寫的Transactional註解的設置 建立一個新事務
- 選用JDBC鏈接 而後建立 SqlSession
- 爲SqlSession註冊事務同步
- SQL操做
- 而後把事務提交
- 最後釋放資源。完成方法調用
- 接口返回200
經過日誌可看出咱們使用MyBatis+Druid 配置數據源 事務管理實際是使用的DataSourceTransactionManager.
雖然代碼中指定了transactionManager,但實際卻沒有增長任何Bean註冊或者其餘有關DataSource的事務管理器代碼。這個地方是由於Spring幫你作了,我設置transactionManager是由於自動裝配的transactionManager名字就是這個。寫出來提醒一下有這個配置,這個配置顧名思義就是配置對這個事務的管理器實現。簡而言之就是PlatformTransactionManager的指定。後面多數據源的時候咱們會經過指定不一樣的transactionManager來實現事務管理。
若是報錯會是怎麼樣呢?
先看日誌(開啓DEBUG日誌):
o.s.web.servlet.DispatcherServlet : GET "/api/account/transfer/annotation?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to io.ilss.transaction.onedatasource.web.AccountController#transferAnnotation(Long, Long, String)
o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [io.ilss.transaction.onedatasource.service.impl.AccountServiceImpl.transferAnnotation]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'transactionManager',-java.lang.Exception
o.s.j.d.DataSourceTransactionManager : Acquired Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] for JDBC transaction
o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] to manual commit
org.mybatis.spring.SqlSessionUtils : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
o.m.s.t.SpringManagedTransaction : JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] will be managed by Spring
i.i.t.o.d.AccountDAO.selectByPrimaryKey : ==> Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey : ==> Parameters: 1(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey : <== Total: 1
org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58] from current transaction
i.i.t.o.d.AccountDAO.selectByPrimaryKey : ==> Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey : ==> Parameters: 2(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey : <== Total: 1
org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective : ==> Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective : ==> Parameters: 小一(String), xiaoyi(String), 123456(String), 754.00(BigDecimal), 2020-01-09T17:04:28(LocalDateTime), 2020-01-09T17:44:33(LocalDateTime), 1(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective : <== Updates: 1
org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback
o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec]
o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] after transaction
o.s.web.servlet.DispatcherServlet : Failed to complete request: java.lang.RuntimeException: annotation error!
o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is
java.lang.RuntimeException: annotation error!
.......
o.a.c.c.C.[Tomcat].[localhost] : Processing ErrorPage[errorCode=0, location=/error]
o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, text/html;q=0.8]
o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 500
複製代碼
- GET 請求到 /api/account/transfer/annotation接口 而後調用service方法
- 根據寫的Transactional註解的設置 建立一個新事務
- 選用JDBC鏈接 而後建立 SqlSession
- 爲SqlSession註冊事務同步
- SQL操做:能夠從日誌看到,執行到第一個跟新操做發生錯誤
- 直接回滾Rolling back JDBC transaction on Connection後打印錯誤日誌。後面的第二個更新操做也就沒了
- 方法異常退出,接口500
- 建立事務定義TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
- 設置事務屬性:如傳播屬性、隔離屬性等
- 經過TransactionManager開啓事務
- 執行業務代碼
- 提交事務 / 處理回滾
// todo: 下一篇多數據源事務管理