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        connMap.put(ds, connection);
053    }
054
055
056    public static Boolean exec(Supplier<Boolean> supplier, Propagation propagation) {
057        //上一级事务的id,支持事务嵌套
058        String currentXID = TransactionContext.getXID();
059        try {
060            switch (propagation) {
061                //若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务
062                case REQUIRED:
063                    if (currentXID != null) {
064                        return supplier.get();
065                    } else {
066                        return execNewTransactional(supplier);
067                    }
068
069
070                    //若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行
071                case SUPPORTS:
072                    return supplier.get();
073
074
075                //若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行
076                case MANDATORY:
077                    if (currentXID != null) {
078                        return supplier.get();
079                    } else {
080                        throw new TransactionException("No existing transaction found for transaction marked with propagation 'mandatory'");
081                    }
082
083
084                    //始终以新事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。
085                case REQUIRES_NEW:
086                    return execNewTransactional(supplier);
087
088
089                //以非事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。
090                case NOT_SUPPORTED:
091                    if (currentXID != null) {
092                        TransactionContext.release();
093                    }
094                    return supplier.get();
095
096
097                //以非事务的方式运行,若存在当前事务,则抛出异常。
098                case NEVER:
099                    if (currentXID != null) {
100                        throw new TransactionException("Existing transaction found for transaction marked with propagation 'never'");
101                    }
102                    return supplier.get();
103
104
105                //暂时不支持这种事务传递方式
106                //default 为 nested 方式
107                default:
108                    throw new TransactionException("Transaction manager does not allow nested transactions");
109
110            }
111        } finally {
112            //恢复上一级事务
113            if (currentXID != null) {
114                TransactionContext.hold(currentXID);
115            }
116        }
117    }
118
119    private static Boolean execNewTransactional(Supplier<Boolean> supplier) {
120        String xid = startTransactional();
121        Boolean success = false;
122        boolean rollbacked = false;
123        try {
124            success = supplier.get();
125        } catch (Throwable e) {
126            rollbacked = true;
127            rollback(xid);
128            throw new TransactionException(e.getMessage(), e);
129        } finally {
130            if (success != null && success) {
131                commit(xid);
132            } else if (!rollbacked) {
133                rollback(xid);
134            }
135        }
136        return success;
137    }
138
139
140    public static Connection getConnection(String xid, String ds) {
141        Map<String, Connection> connections = CONNECTION_HOLDER.get().get(xid);
142        return connections == null || connections.isEmpty() ? null : connections.get(ds);
143    }
144
145
146    public static String startTransactional() {
147        String xid = UUID.randomUUID().toString();
148        TransactionContext.hold(xid);
149        return xid;
150    }
151
152    public static void commit(String xid) {
153        release(xid, true);
154    }
155
156    public static void rollback(String xid) {
157        release(xid, false);
158    }
159
160    private static void release(String xid, boolean commit) {
161        //先release,才能正常的进行 commit 或者 rollback.
162        TransactionContext.release();
163
164        Exception exception = null;
165        Map<String, Map<String, Connection>> holdMap = CONNECTION_HOLDER.get();
166        try {
167            if (holdMap.isEmpty()) {
168                return;
169            }
170            Map<String, Connection> connections = holdMap.get(xid);
171            for (Connection conn : connections.values()) {
172                try {
173                    if (commit) {
174                        conn.commit();
175                    } else {
176                        conn.rollback();
177                    }
178                } catch (SQLException e) {
179                    exception = e;
180                } finally {
181                    try {
182                        conn.close();
183                    } catch (SQLException e) {
184                        //ignore
185                    }
186                }
187            }
188        } finally {
189            holdMap.remove(xid);
190            if (holdMap.isEmpty()) {
191                CONNECTION_HOLDER.remove();
192            }
193            if (exception != null) {
194                log.error("TransactionalManager.release() is error. cause: " + exception.getMessage(), exception);
195            }
196        }
197    }
198}