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