Sqlite 源码分析 -- SQLiteDatabase 使用事务 (API 14)

注意事项:

  1. 插入单条数据不需要开启事务;

  2. beginTransaction() 获取 mLock 锁后不会释放,直到调用 endTransaction(). 所以在此期间如果有另一个线程进行 CRUD 操作,获取不到 mLock 锁,只能等待;

  3. 执行 sql 语句的正确方式:

db.beginTransaction();
try {
  ...
  // 注意该语句放在 try 语句块的最后,表明最终的操作成功
  db.setTransactionSuccessful();
} finally {
  // 注意该语句放在 finally 语句块中,确定进行 roll back 或 commit
  db.endTransaction();
}

一、beginTransaction() 获取 mLock 锁,且不会释放

1. 开启事务

/**
 * Begins a transaction in EXCLUSIVE mode.
 * <p>
 * Transactions can be nested.
 * When the outer transaction is ended all of
 * the work done in that transaction and all of the nested transactions will be committed or
 * rolled back. The changes will be rolled back if any transaction is ended without being
 * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
 * </p>
 * <p>Here is the standard idiom for transactions:
 *
 * beginTransaction() 的正确使用方式:
 *
 * <pre>
 *   db.beginTransaction();
 *   try {
 *     ...
 *     // 注意该语句放在 try 语句块的最后,表明最终的操作成功
 *     db.setTransactionSuccessful();
 *   } finally {
 *     // 注意该语句放在 finally 语句块中,进行 roll back 或 commit
 *     db.endTransaction();
 *   }
 * </pre>
 */
public void beginTransaction() {
    beginTransaction(null /* transactionStatusCallback */, true);
}

2. 获取 mLock 锁,然后执行 execSQL("BEGIN EXCLUSIVE;");

private void beginTransaction(SQLiteTransactionListener transactionListener, boolean exclusive) {
    verifyDbIsOpen();
    // 强制获取 mLock (即使是单线程), 当前线程循环等待获取 mLock 锁,直到获取为止
    // beginTransaction() 执行成功后不会释放该锁, 锁在 endTransaction() 中释放
    lockForced(BEGIN_SQL);
    boolean ok = false;
    try {
        // If this thread already had the lock then get out
        if (mLock.getHoldCount() > 1) {
            if (mInnerTransactionIsSuccessful) {
                // 在 setTransactionSuccessful() 和 endTransaction() 语句中间不允许调用 beginTransaction()
                String msg = "Cannot call beginTransaction between " + "calling setTransactionSuccessful and endTransaction";
                IllegalStateException e = new IllegalStateException(msg);
                Log.e(TAG, "beginTransaction() failed", e);
                throw e;
            }
            ok = true;
            // 如果当前线程之前已经持有了 mLock, 则直接返回, (意味着下面的操作已经执行过了,无需再执行)
            return;
        }
        // 该线程第一次持有 mLock (mLock.getHoldCount() == 1)
        // This thread didn't already have the lock, so begin a database transaction now.
        if (exclusive && mConnectionPool == null) {
            // beginTransaction() 走这里
            execSQL("BEGIN EXCLUSIVE;");
        } else {
            execSQL("BEGIN IMMEDIATE;");
        }
        mTransStartTime = SystemClock.uptimeMillis();
        mTransactionListener = transactionListener;
        mTransactionIsSuccessful = true;
        mInnerTransactionIsSuccessful = false;
        if (transactionListener != null) {
            try {
                transactionListener.onBegin();
            } catch (RuntimeException e) {
                execSQL("ROLLBACK;");
                throw e;
            }
        }
        ok = true;
    } finally {
        if (!ok) {
            // beginTransaction is called before the try block so we must release the lock in the case of failure.
            // beginTransaction 成功后不会释放锁, 锁在 endTransaction 中释放
            unlockForced();
        }
    }
}

3. 通过 SQLiteStatement 执行 executeUpdateDelete

public void execSQL(String sql) throws SQLException {
    executeSql(sql, null);
}

private int executeSql(String sql, Object[] bindArgs) throws SQLException {
    if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) {
        // "BEGIN ..." 不会跳入该逻辑
        disableWriteAheadLogging();
        mHasAttachedDbs = true;
    }

    // 通过 SQLiteStatement 执行 executeUpdateDelete
    SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs);
    try {
        return statement.executeUpdateDelete();
    } catch (SQLiteDatabaseCorruptException e) {
        onCorruption();
        throw e;
    } finally {
        statement.close();
    }
}

4. 调用 native 方法 native_executeSql(mSql)

/**
 * 顾名思义,executeUpdateDelete(), SQLiteDatabase 调用 update() 和 delete() 方法时会走这里
 * 调用 beginTransaction() 也会走这里
 *
 * Execute this SQL statement, if the the number of rows affected by execution of this SQL
 * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
 *
 * @return the number of rows affected by this SQL statement execution.
 * @throws android.database.SQLException If the SQL string is invalid for
 *         some reason
 */
public int executeUpdateDelete() {
    try {
        // 缓存 STATEMENT_UPDATE 和 STATEMENT_BEGIN 类型的 sql 语句
        saveSqlAsLastSqlStatement();
        // 执行每个 sql 语句前都会执行该方法
        // 会导致当前线程循环等待获取 SQLiteDatabase 中的 mLock 锁,直到获取为止
        acquireAndLock(WRITE);
        int numChanges = 0;
        if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) {
            // beginTransaction() 会走这里
            // since the statement doesn't have to be prepared,
            // call the following native method which will not prepare
            // the query plan
            native_executeSql(mSql);
        } else {
            // update() 和 delete() 会走这里
            numChanges = native_execute();
        }
        return numChanges;
    } finally {
        // beginTransaction() 不会释放 mLock 锁,, 隐式事务会释放锁
        releaseAndUnlock();
    }
}

5. 当前线程循环等待获取 SQLiteDatabase 中的 mLock 锁

/**
 * 执行每个 sql 语句前都会执行该方法
 * 会导致当前线程循环等待获取 SQLiteDatabase 中的 mLock 锁,直到获取为止
 *
 * Called before every method in this class before executing a SQL statement,
 * this method does the following:
 * <ul>
 *   <li>make sure the database is open</li>
 *   <li>get a database connection from the connection pool,if possible</li>
 *   <li>notifies {@link BlockGuard} of read/write</li>
 *   <li>if the SQL statement is an update, start transaction if not already in one.
 *   otherwise, get lock on the database</li>
 *   <li>acquire reference on this object</li>
 *   <li>and then return the current time _after_ the database lock was acquired</li>
 * </ul>
 * <p>
 * This method removes the duplicate code from the other public
 * methods in this class.
 */
private long acquireAndLock(boolean rwFlag) {
    mState = 0;
    // use pooled database connection handles for SELECT SQL statements
    mDatabase.verifyDbIsOpen();
    // 获取的 db 仍然是 mDatabase
    SQLiteDatabase db = ((mStatementType & SQLiteProgram.STATEMENT_USE_POOLED_CONN) > 0) ? mDatabase.getDbConnection(mSql) : mDatabase;
    // use the database connection obtained above
    mOrigDb = mDatabase;
    mDatabase = db;
    setNativeHandle(mDatabase.mNativeHandle);
    if (rwFlag == WRITE) {
        BlockGuard.getThreadPolicy().onWriteToDisk();
    } else {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }

    /*
     * Special case handling of SQLiteDatabase.execSQL("BEGIN transaction").
     * we know it is execSQL("BEGIN transaction") from the caller IF there is no lock held.
     * beginTransaction() methods in SQLiteDatabase call lockForced() before
     * calling execSQL("BEGIN transaction").
     */
    if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_BEGIN) {
        if (!mDatabase.isDbLockedByCurrentThread()) {
            // transaction is  NOT started by calling beginTransaction() methods in
            // SQLiteDatabase
            mDatabase.setTransactionUsingExecSqlFlag();
        }
    } else if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_UPDATE) {
        // got update SQL statement. if there is NO pending transaction, start one
        if (!mDatabase.inTransaction()) {
            mDatabase.beginTransactionNonExclusive();
            mState = TRANS_STARTED;
        }
    }
    // do I have database lock? if not, grab it.
    if (!mDatabase.isDbLockedByCurrentThread()) {
        // 当前线程循环等待获取 SQLiteDatabase 中的 mLock 锁,直到获取为止
        mDatabase.lock(mSql);
        mState = LOCK_ACQUIRED;
    }

    acquireReference();
    long startTime = SystemClock.uptimeMillis();
    mDatabase.closePendingStatements();
    compileAndbindAllArgs();
    return startTime;
}

6. beginTransaction() 不会释放 mLock 锁,, 隐式事务会释放锁

/**
 * this method releases locks and references acquired in {@link #acquireAndLock(boolean)}
 */
private void releaseAndUnlock() {
    releaseReference();
    if (mState == TRANS_STARTED) {
        try {
            mDatabase.setTransactionSuccessful();
        } finally {
            mDatabase.endTransaction();
        }
    } else if (mState == LOCK_ACQUIRED) {
        // beginTransaction() 不会走这里, 隐式事务会走这里, 即 beginTransaction() 不会释放 mLock 锁
        mDatabase.unlock();
    }
    if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_COMMIT
            || (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_ABORT) {
        mDatabase.resetTransactionUsingExecSqlFlag();
    }
    clearBindings();
    // release the compiled sql statement so that the caller's SQLiteStatement no longer
    // has a hard reference to a database object that may get deallocated at any point.
    release();
    // restore the database connection handle to the original value
    mDatabase = mOrigDb;
    setNativeHandle(mDatabase.mNativeHandle);
}

二、setTransactionSuccessful() 设置操作成功标识

/**
 * setTransactionSuccessful() 与 endTransaction() 之间尽量不要做别的事情
 *
 * Marks the current transaction as successful. Do not do any more database work between
 * calling this and calling endTransaction. Do as little non-database work as possible in that
 * situation too. If any errors are encountered between this and endTransaction the transaction
 * will still be committed.
 *
 * @throws IllegalStateException if the current thread is not in a transaction or the
 * transaction is already marked as successful.
 */
public void setTransactionSuccessful() {
    verifyDbIsOpen();
    if (!mLock.isHeldByCurrentThread()) {
        throw new IllegalStateException("no transaction pending");
    }
    if (mInnerTransactionIsSuccessful) {
        throw new IllegalStateException(
                "setTransactionSuccessful may only be called once per call to beginTransaction");
    }
    mInnerTransactionIsSuccessful = true;
}

三、endTransaction() 进行 COMMIT 或 ROLLBACK,释放 mLock 锁

/**
 * End a transaction. See beginTransaction for notes about how to use this and when transactions
 * are committed and rolled back.
 */
public void endTransaction() {
    // 如 mLock 锁没有被当前线程占据,则抛出异常(从 beginTransaction 开始,mLock 就一直被当前线程占据)
    verifyLockOwner();
    try {
        if (mInnerTransactionIsSuccessful) { // 事务中的操作全部执行成功的情况
            mInnerTransactionIsSuccessful = false;
        } else {
            mTransactionIsSuccessful = false;
        }
        if (mLock.getHoldCount() != 1) {
            return;
        }
        RuntimeException savedException = null;
        if (mTransactionListener != null) {
            try {
                if (mTransactionIsSuccessful) {
                    mTransactionListener.onCommit();
                } else {
                    mTransactionListener.onRollback();
                }
            } catch (RuntimeException e) {
                savedException = e;
                mTransactionIsSuccessful = false;
            }
        }
        if (mTransactionIsSuccessful) { // beginTransaction() 中被设置为 true
            // 执行 "COMMIT;"
            execSQL(COMMIT_SQL);
            // if write-ahead logging is used, we have to take care of checkpoint.
            // TODO: should applications be given the flexibility of choosing when to
            // trigger checkpoint?
            // for now, do checkpoint after every COMMIT because that is the fastest
            // way to guarantee that readers will see latest data.
            // but this is the slowest way to run sqlite with in write-ahead logging mode.
            if (this.mConnectionPool != null) {
                execSQL("PRAGMA wal_checkpoint;");
                if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
                    Log.i(TAG, "PRAGMA wal_Checkpoint done");
                }
            }
            // log the transaction time to the Eventlog.
            if (ENABLE_DB_SAMPLE) {
                logTimeStat(getLastSqlStatement(), mTransStartTime, COMMIT_SQL);
            }
        } else {
            try {
                // 执行 回滚
                execSQL("ROLLBACK;");
                if (savedException != null) {
                    throw savedException;
                }
            } catch (SQLException e) {
                if (false) {
                    Log.d(TAG, "exception during rollback, maybe the DB previously " + "performed an auto-rollback");
                }
            }
        }
    } finally {
        mTransactionListener = null;
        // 释放从 beginTransaction() 就一直占据的 mLock 锁
        unlockForced();
        if (false) {
            Log.v(TAG, "unlocked " + Thread.currentThread() + ", holdCount is " + mLock.getHoldCount());
        }
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容