001/**
002 * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package com.mybatisflex.core.transaction;
017
018import org.apache.ibatis.logging.Log;
019import org.apache.ibatis.logging.LogFactory;
020
021import java.sql.Connection;
022import java.sql.SQLException;
023import java.util.Map;
024import java.util.UUID;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.function.Supplier;
027
028/**
029 * 事务管理器
030 */
031public class TransactionalManager {
032
033    private static final Log log = LogFactory.getLog(TransactionalManager.class);
034
035    //<xid : <datasource : connection>>
036    private static final ThreadLocal<Map<String, Map<String, Connection>>> CONNECTION_HOLDER
037            = ThreadLocal.withInitial(ConcurrentHashMap::new);
038
039
040    public static void hold(String xid, String ds, Connection connection) {
041        Map<String, Map<String, Connection>> holdMap = CONNECTION_HOLDER.get();
042        Map<String, Connection> connMap = holdMap.get(xid);
043        if (connMap == null) {
044            connMap = new ConcurrentHashMap<>();
045            holdMap.put(xid, connMap);
046        }
047
048        if (connMap.containsKey(ds)) {
049            return;
050        }
051
052        try {
053            connection.setAutoCommit(false);
054        } catch (SQLException e) {
055            if (log.isDebugEnabled()) {
056                log.debug("Error set AutoCommit to false.  Cause: " + e);
057            }
058        }
059        connMap.put(ds, connection);
060    }
061
062
063    public static Boolean exec(Supplier<Boolean> supplier, Propagation propagation) {
064        //上一级事务的id,支持事务嵌套
065        String currentXID = TransactionContext.getXID();
066        try {
067            switch (propagation) {
068                //若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务
069                case REQUIRED:
070                    if (currentXID != null) {
071                        return supplier.get();
072                    } else {
073                        return execNewTransactional(supplier);
074                    }
075
076
077                    //若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行
078                case SUPPORTS:
079                    return supplier.get();
080
081
082                //若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行
083                case MANDATORY:
084                    if (currentXID != null) {
085                        return supplier.get();
086                    } else {
087                        throw new TransactionException("No existing transaction found for transaction marked with propagation 'mandatory'");
088                    }
089
090
091                    //始终以新事物的方式运行,若存在当前事务,则暂停(挂起)当前事务。
092                case REQUIRES_NEW:
093                    return execNewTransactional(supplier);
094
095
096                //以非事物的方式运行,若存在当前事务,则暂停(挂起)当前事务。
097                case NOT_SUPPORTED:
098                    if (currentXID != null) {
099                        TransactionContext.release();
100                    }
101                    return supplier.get();
102
103
104                //以非事物的方式运行,若存在当前事务,则抛出异常。
105                case NEVER:
106                    if (currentXID != null) {
107                        throw new TransactionException("Existing transaction found for transaction marked with propagation 'never'");
108                    }
109                    return supplier.get();
110
111
112                //暂时不支持这种事务传递方式
113                //default 为 nested 方式
114                default:
115                    throw new TransactionException("Transaction manager does not allow nested transactions");
116
117            }
118        } finally {
119            //恢复上一级事务
120            if (currentXID != null) {
121                TransactionContext.hold(currentXID);
122            }
123        }
124    }
125
126    private static Boolean execNewTransactional(Supplier<Boolean> supplier) {
127        String xid = startTransactional();
128        Boolean success = false;
129        boolean rollbacked = false;
130        try {
131            success = supplier.get();
132        } catch (Exception e) {
133            rollbacked = true;
134            rollback(xid);
135            throw new TransactionException(e.getMessage(), e);
136        } finally {
137            if (success != null && success) {
138                commit(xid);
139            } else if (!rollbacked) {
140                rollback(xid);
141            }
142        }
143        return success;
144    }
145
146
147    public static Connection getConnection(String xid, String ds) {
148        Map<String, Connection> connections = CONNECTION_HOLDER.get().get(xid);
149        return connections == null || connections.isEmpty() ? null : connections.get(ds);
150    }
151
152
153    public static String startTransactional() {
154        String xid = UUID.randomUUID().toString();
155        TransactionContext.hold(xid);
156        return xid;
157    }
158
159    public static void commit(String xid) {
160        release(xid, true);
161    }
162
163    public static void rollback(String xid) {
164        release(xid, false);
165    }
166
167    private static void release(String xid, boolean commit) {
168        //先release,才能正常的进行 commit 或者 rollback.
169        TransactionContext.release();
170
171        Exception exception = null;
172        Map<String, Map<String, Connection>> holdMap = CONNECTION_HOLDER.get();
173        try {
174            if (holdMap.isEmpty()) {
175                return;
176            }
177            Map<String, Connection> connections = holdMap.get(xid);
178            for (Connection conn : connections.values()) {
179                try {
180                    if (commit) {
181                        conn.commit();
182                    } else {
183                        conn.rollback();
184                    }
185                } catch (SQLException e) {
186                    exception = e;
187                } finally {
188                    try {
189                        conn.close();
190                    } catch (SQLException e) {
191                        //ignore
192                    }
193                }
194            }
195        } finally {
196            holdMap.remove(xid);
197            if (holdMap.isEmpty()) {
198                CONNECTION_HOLDER.remove();
199            }
200            if (exception != null) {
201                log.error("TransactionalManager.release() is error. cause: " + exception.getMessage(), exception);
202            }
203        }
204    }
205}