MySQL中沒有MVCC機(jī)制的影響分析與替代方案
1. MVCC 簡介及其在 MySQL 中的重要性
1.1 什么是 MVCC
多版本并發(fā)控制(MVCC)是現(xiàn)代數(shù)據(jù)庫系統(tǒng)中廣泛使用的一種并發(fā)控制機(jī)制。與傳統(tǒng)的鎖機(jī)制不同,MVCC 通過維護(hù)數(shù)據(jù)的多個版本來實現(xiàn)非阻塞讀操作,從而大大提高數(shù)據(jù)庫的并發(fā)性能。
在 MVCC 中,當(dāng)數(shù)據(jù)被修改時,數(shù)據(jù)庫不會直接覆蓋原有數(shù)據(jù),而是創(chuàng)建數(shù)據(jù)的新版本。這樣,不同的事務(wù)可以看到數(shù)據(jù)在不同時間點的快照,從而避免了讀寫沖突。
1.2 MySQL 中的 MVCC 實現(xiàn)
MySQL 的 InnoDB 存儲引擎通過以下機(jī)制實現(xiàn) MVCC:
隱藏列:每行數(shù)據(jù)包含兩個隱藏列
DB_TRX_ID:最近修改該行數(shù)據(jù)的事務(wù) IDDB_ROLL_PTR:指向 undo log 中舊版本數(shù)據(jù)的指針DB_ROW_ID(可選):行 ID
Read View:事務(wù)在執(zhí)行時創(chuàng)建的一致性讀視圖,決定了該事務(wù)能看到哪些數(shù)據(jù)版本
Undo Log:存儲數(shù)據(jù)的歷史版本,用于構(gòu)建數(shù)據(jù)快照和回滾操作
2. 沒有 MVCC 的 MySQL 會面臨的問題
2.1 并發(fā)性能大幅下降
沒有 MVCC,MySQL 將不得不依賴更嚴(yán)格的鎖機(jī)制來處理并發(fā)訪問。
-- 在沒有 MVCC 的情況下,簡單的查詢也可能導(dǎo)致阻塞 -- 事務(wù)1 START TRANSACTION; UPDATE users SET balance = balance - 100 WHERE id = 1; -- 此行被鎖定... -- 事務(wù)2(會被阻塞) START TRANSACTION; SELECT * FROM users WHERE id = 1; -- 這個查詢會被阻塞,等待事務(wù)1提交 COMMIT;
2.2 常見的并發(fā)問題
2.2.1 臟讀(Dirty Read)
-- 事務(wù)1 START TRANSACTION; UPDATE accounts SET balance = balance + 100 WHERE user_id = 1; -- 在沒有 MVCC 和適當(dāng)隔離級別的情況下 -- 事務(wù)2 START TRANSACTION; SELECT balance FROM accounts WHERE user_id = 1; -- 可能讀取到未提交的 100 COMMIT; -- 如果事務(wù)1回滾 ROLLBACK; -- 事務(wù)2讀取的數(shù)據(jù)就是無效的
2.2.2 不可重復(fù)讀(Non-repeatable Read)
-- 事務(wù)1 START TRANSACTION; SELECT * FROM products WHERE id = 1; -- 返回 price = 100 -- 事務(wù)2 START TRANSACTION; UPDATE products SET price = 120 WHERE id = 1; COMMIT; -- 事務(wù)1再次查詢 SELECT * FROM products WHERE id = 1; -- 現(xiàn)在返回 price = 120 -- 同一事務(wù)中兩次查詢結(jié)果不一致 COMMIT;
2.2.3 幻讀(Phantom Read)
-- 事務(wù)1 START TRANSACTION; SELECT COUNT(*) FROM orders WHERE user_id = 1 AND status = 'pending'; -- 返回 5 -- 事務(wù)2 START TRANSACTION; INSERT INTO orders (user_id, status, amount) VALUES (1, 'pending', 50); COMMIT; -- 事務(wù)1再次查詢 SELECT COUNT(*) FROM orders WHERE user_id = 1 AND status = 'pending'; -- 返回 6 -- 出現(xiàn)了幻影行 COMMIT;
3. 沒有 MVCC 時的替代解決方案
3.1 基于鎖的并發(fā)控制
-- 使用表級鎖保證一致性 -- 事務(wù)1 LOCK TABLES accounts WRITE; START TRANSACTION; SELECT balance FROM accounts WHERE user_id = 1; UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; COMMIT; UNLOCK TABLES; -- 事務(wù)2(必須等待) LOCK TABLES accounts WRITE; -- 等待事務(wù)1釋放鎖 START TRANSACTION; SELECT balance FROM accounts WHERE user_id = 1; -- ...
3.2 應(yīng)用層并發(fā)控制
// Java 示例:使用應(yīng)用層樂觀鎖
public class AccountService {
public boolean transferMoney(int fromUserId, int toUserId, BigDecimal amount) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 使用版本號實現(xiàn)樂觀鎖
String selectSql = "SELECT id, balance, version FROM accounts WHERE user_id = ? FOR UPDATE";
PreparedStatement stmt1 = conn.prepareStatement(selectSql);
stmt1.setInt(1, fromUserId);
ResultSet rs = stmt1.executeQuery();
if (rs.next()) {
BigDecimal currentBalance = rs.getBigDecimal("balance");
int currentVersion = rs.getInt("version");
if (currentBalance.compareTo(amount) < 0) {
conn.rollback();
return false; // 余額不足
}
// 更新賬戶
String updateSql = "UPDATE accounts SET balance = balance - ?, version = version + 1 " +
"WHERE user_id = ? AND version = ?";
PreparedStatement stmt2 = conn.prepareStatement(updateSql);
stmt2.setBigDecimal(1, amount);
stmt2.setInt(2, fromUserId);
stmt2.setInt(3, currentVersion);
int rowsAffected = stmt2.executeUpdate();
if (rowsAffected == 0) {
// 版本號不匹配,說明數(shù)據(jù)已被其他事務(wù)修改
conn.rollback();
return false; // 需要重試
}
conn.commit();
return true;
}
} catch (SQLException e) {
if (conn != null) {
try { conn.rollback(); } catch (SQLException ex) {}
}
throw new RuntimeException("Transfer failed", e);
} finally {
if (conn != null) {
try { conn.close(); } catch (SQLException e) {}
}
}
return false;
}
}
3.3 使用 Redis 分布式鎖
// 使用 Redis 實現(xiàn)分布式鎖來處理并發(fā)
public class DistributedAccountService {
private JedisPool jedisPool;
private DataSource dataSource;
public boolean transferWithDistributedLock(int fromUserId, int toUserId, BigDecimal amount) {
String lockKey = "account_lock:" + fromUserId;
String lockValue = UUID.randomUUID().toString();
Jedis jedis = null;
Connection conn = null;
try {
jedis = jedisPool.getResource();
// 獲取分布式鎖
boolean locked = false;
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 5000) { // 5秒超時
if ("OK".equals(jedis.set(lockKey, lockValue, "NX", "PX", 30000))) {
locked = true;
break;
}
Thread.sleep(100); // 短暫等待后重試
}
if (!locked) {
throw new RuntimeException("Acquire lock timeout");
}
// 執(zhí)行轉(zhuǎn)賬操作
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// ... 轉(zhuǎn)賬邏輯
conn.commit();
return true;
} catch (Exception e) {
if (conn != null) {
try { conn.rollback(); } catch (SQLException ex) {}
}
throw new RuntimeException("Transfer failed", e);
} finally {
if (jedis != null) {
// 使用 Lua 腳本保證原子性地釋放鎖
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
jedis.eval(luaScript, 1, lockKey, lockValue);
jedis.close();
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) {}
}
}
}
}
4. 性能對比分析
4.1 測試場景設(shè)計
-- 創(chuàng)建測試表
CREATE TABLE performance_test (
id INT PRIMARY KEY AUTO_INCREMENT,
data VARCHAR(1000),
counter INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入測試數(shù)據(jù)
DELIMITER $$
CREATE PROCEDURE InsertTestData(IN numRecords INT)
BEGIN
DECLARE i INT DEFAULT 0;
WHILE i < numRecords DO
INSERT INTO performance_test (data) VALUES (REPEAT('X', 1000));
SET i = i + 1;
END WHILE;
END$$
DELIMITER ;
CALL InsertTestData(10000);
4.2 并發(fā)測試代碼
// Java 并發(fā)測試
public class ConcurrentTest {
private static final int THREAD_COUNT = 50;
private static final int OPERATIONS_PER_THREAD = 100;
private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT);
private static final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
private static final AtomicLong successCount = new AtomicLong(0);
private static final AtomicLong failureCount = new AtomicLong(0);
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
long startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_COUNT; i++) {
executor.execute(new Worker(i));
}
latch.await();
executor.shutdown();
long endTime = System.currentTimeMillis();
System.out.println("總執(zhí)行時間: " + (endTime - startTime) + "ms");
System.out.println("成功操作: " + successCount.get());
System.out.println("失敗操作: " + failureCount.get());
System.out.println("吞吐量: " + (successCount.get() * 1000.0 / (endTime - startTime)) + " ops/sec");
}
static class Worker implements Runnable {
private final int workerId;
Worker(int workerId) {
this.workerId = workerId;
}
@Override
public void run() {
try {
barrier.await(); // 所有線程同時開始
for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
if (performOperation()) {
successCount.incrementAndGet();
} else {
failureCount.incrementAndGet();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}
private boolean performOperation() {
// 執(zhí)行數(shù)據(jù)庫操作
// 1. 有 MVCC 的情況:使用普通事務(wù)
// 2. 沒有 MVCC 的情況:使用悲觀鎖或樂觀鎖
return true;
}
}
}
5. 系統(tǒng)架構(gòu)調(diào)整方案
5.1 讀寫分離架構(gòu)
┌─────────────────┐ ┌──────────────────┐
│ 應(yīng)用服務(wù)器層 │ │ 數(shù)據(jù)庫代理層 │
│ │ │ │
│ ┌─────────────┐ │ │ ┌──────────────┐ │
│ │ Web應(yīng)用 │──┼─────?│ Proxy │ │
│ └─────────────┘ │ │ │ (如MyCat) │ │
│ ┌─────────────┐ │ │ └──────────────┘ │
│ │ API服務(wù) │──┼─────┘ │ │
│ └─────────────┘ │ │ │
└─────────────────┘ ▼ ▼
┌─────────────────────────────────┐
│ 數(shù)據(jù)庫層 │
│ │
│ ┌─────────────┐ ┌───────────┐ │
│ │ 主數(shù)據(jù)庫 │ │ 從數(shù)據(jù)庫 │ │
│ │ (寫操作) │ │ (讀操作) │ │
│ └─────────────┘ └───────────┘ │
└─────────────────────────────────┘
5.2 分庫分表策略
// 分庫分表示例
public class ShardingService {
private static final int DB_COUNT = 4;
private static final int TABLE_COUNT_PER_DB = 8;
public ShardingResult calculateSharding(long userId) {
// 分庫:userId % DB_COUNT
int dbIndex = (int) (userId % DB_COUNT);
String dbName = "user_db_" + dbIndex;
// 分表:userId / DB_COUNT % TABLE_COUNT_PER_DB
int tableIndex = (int) (userId / DB_COUNT % TABLE_COUNT_PER_DB);
String tableName = "user_info_" + tableIndex;
return new ShardingResult(dbName, tableName);
}
public static class ShardingResult {
public final String dbName;
public final String tableName;
public ShardingResult(String dbName, String tableName) {
this.dbName = dbName;
this.tableName = tableName;
}
}
}
6. 監(jiān)控和調(diào)優(yōu)策略
6.1 鎖監(jiān)控
-- 監(jiān)控當(dāng)前鎖情況
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
-- 查看當(dāng)前活動事務(wù)
SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started DESC;
6.2 性能監(jiān)控腳本
-- 創(chuàng)建監(jiān)控表
CREATE TABLE lock_monitor (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sample_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
lock_wait_count INT,
long_running_trx_count INT,
deadlock_count INT
);
-- 定期收集監(jiān)控數(shù)據(jù)
DELIMITER $$
CREATE PROCEDURE CollectLockStats()
BEGIN
INSERT INTO lock_monitor (lock_wait_count, long_running_trx_count, deadlock_count)
SELECT
(SELECT COUNT(*) FROM information_schema.innodb_lock_waits),
(SELECT COUNT(*) FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60),
(SELECT VARIABLE_VALUE FROM information_schema.global_status
WHERE VARIABLE_NAME = 'innodb_deadlocks');
END$$
DELIMITER ;
-- 創(chuàng)建事件定期執(zhí)行
CREATE EVENT monitor_lock_event
ON SCHEDULE EVERY 1 MINUTE
DO CALL CollectLockStats();
7. 總結(jié)
如果沒有 MVCC 機(jī)制,MySQL 將面臨嚴(yán)重的并發(fā)性能問題。為了維持?jǐn)?shù)據(jù)一致性,系統(tǒng)將不得不依賴更嚴(yán)格的鎖機(jī)制,這會導(dǎo)致:
- 吞吐量大幅下降:大量的鎖等待會限制系統(tǒng)并發(fā)處理能力
- 響應(yīng)時間增加:讀操作可能被寫操作阻塞
- 死鎖風(fēng)險增加:復(fù)雜的鎖依賴關(guān)系容易導(dǎo)致死鎖
- 系統(tǒng)復(fù)雜性提高:需要在應(yīng)用層實現(xiàn)復(fù)雜的并發(fā)控制邏輯
雖然可以通過讀寫分離、分庫分表、應(yīng)用層鎖等方案來緩解問題,但這些方案都會增加系統(tǒng)的復(fù)雜性和維護(hù)成本。MVCC 機(jī)制在保證數(shù)據(jù)一致性的同時提供了優(yōu)異的并發(fā)性能,是現(xiàn)代數(shù)據(jù)庫系統(tǒng)不可或缺的重要特性。
在實際系統(tǒng)設(shè)計中,我們應(yīng)該充分理解 MVCC 的工作原理,合理設(shè)置事務(wù)隔離級別,并在必要時配合使用適當(dāng)?shù)逆i策略,才能在數(shù)據(jù)一致性和系統(tǒng)性能之間找到最佳平衡點。
以上就是MySQL中沒有MVCC機(jī)制的影響分析與替代方案的詳細(xì)內(nèi)容,更多關(guān)于MySQL沒有MVCC機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
jxl 導(dǎo)出數(shù)據(jù)到excel的實例講解
下面小編就為大家分享一篇jxl 導(dǎo)出數(shù)據(jù)到excel的實例講解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-12-12
mybatis-plus分頁如何接收前端參數(shù)limit和page
這篇文章主要介紹了mybatis-plus分頁如何接收前端參數(shù)limit和page,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01
Spring Boot 整合 TKMybatis 二次簡化持久層代碼的實現(xiàn)
這篇文章主要介紹了Spring Boot 整合 TKMybatis 二次簡化持久層代碼的實現(xiàn),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01
JPA如何設(shè)置表名和實體名,表字段與實體字段的對應(yīng)
這篇文章主要介紹了JPA如何設(shè)置表名和實體名,表字段與實體字段的對應(yīng),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
Sharding-JDBC自動實現(xiàn)MySQL讀寫分離的示例代碼
本文主要介紹了Sharding-JDBC自動實現(xiàn)MySQL讀寫分離,優(yōu)點在于數(shù)據(jù)源完全有Sharding-JDBC托管,寫操作自動執(zhí)行master庫,讀操作自動執(zhí)行slave庫,感興趣的可以了解一下2021-11-11

