先谈谈 MVCC
两阶段锁(2PL)作为一种经典的并发控制机制,确实有效地解决了数据库中的读写冲突问题,保障了事务的隔离性。然而,锁机制本身在高并发场景下会带来一定的性能开销,例如阻塞、死锁以及上下文切换等问题。这类似于 Java 并发编程的发展历程:从早期的 synchronized 关键字,逐步演进到更灵活的 Lock 接口和读写锁(ReentrantReadWriteLock)等机制。数据库领域也面临着类似的挑战与演进需求,于是便催生了另一种解决并发问题的思路------多版本并发控制(MVCC, Multi-Version Concurrency Control)。MVCC 通过维护数据的多个版本来实现非阻塞的读操作,在提升并发性能的同时,有效减少了锁的使用,成为现代数据库系统中实现高并发访问的重要技术之一。
几个日志的作用
- binlog:记录"做了什么",用于复制和恢复。
- redo log:记录"怎么改的页",用于崩溃后重做,保证持久性。
- undo log:记录"改之前什么样",用于回滚和 MVCC,保证原子性与隔离性。
MVCC 模拟实现代码总结
该代码实现了一个简化的 多版本并发控制(MVCC) 存储系统,用于演示数据库中如何通过版本管理实现高并发下的读写隔离,避免锁竞争,提升系统吞吐。
1. 核心设计思想
MVCC 机制:通过为每个数据项维护多个历史版本,使得读操作无需加锁,可以读取事务开始时的"快照",而写操作则创建新版本,从而实现读写不阻塞。 版本控制:使用全局递增的 globalVersion 来标识每个写操作的时序,事务以开始时的版本号作为其"视图",读取可见的最新版本。
2. 关键组件说明
arduino
class VersionedValue<T> {
private final T value;
private final long version;
}
// 每个 key 对应多个版本的值
public final Map<String, List<VersionedValue<Integer>>> store = new ConcurrentHashMap<>();
//全局版本号
private final AtomicLong globalVersion = new AtomicLong(1);
public long beginTransaction() {
return globalVersion.get(); // 事务开始时的全局版本号
}
public VersionedValue<Integer> read(String key, long transactionVersion) {
List<VersionedValue<Integer>> versions = store.get(key);
if (versions == null) return new VersionedValue<>(0, 0);
// 选择小于等于事务版本的最新版本
for (int i = versions.size() - 1; i >= 0; i--) {
VersionedValue<Integer> v = versions.get(i);
if (v.getVersion() <= transactionVersion) {
return v;
}
}
return new VersionedValue<>(0, 0);
}
// 冲突检测写入
// 推荐的修改:使用 synchronized 块
public boolean writeWithConflictDetection(String key, int value, long txVersion) {
List<VersionedValue<Integer>> versions = store.computeIfAbsent(key, k -> new ArrayList<>());
// 对特定key的版本列表进行同步
synchronized (versions) {
if (!versions.isEmpty()) {
VersionedValue<Integer> latestVersion = versions.get(versions.size() - 1);
if (latestVersion.getVersion() > txVersion) {
return false; // 冲突
}
}
// 没有冲突,执行写入(注意:write方法也需要被同步,或者将其逻辑合并到这里)
// 为了安全,将 write 方法的逻辑也放入同步块
// 假设 globalVersion 已改为 AtomicLong
VersionedValue<Integer> newVersion = new VersionedValue<>(value, globalVersion.incrementAndGet());
versions.add(newVersion);
return true;
}
}
3. 核心操作逻辑
- beginTransaction(): 返回当前全局版本号作为事务的"快照版本",后续读操作基于此版本可见性规则进行。
- read(String key, long transactionVersion): 从指定 key 的版本列表中,查找 版本号 ≤ 事务版本 的最新版本。 若无数据,返回默认值 (0, 0),模拟"读取未定义值"。
- writeWithConflictDetection(String key, int value, long txVersion): 写前检查(Write-Time Validation):在写入前检查该 key 的最新版本是否已超过当前事务的快照版本。 若存在更新的版本(latestVersion > txVersion),说明有并发写冲突,写入失败。 否则,生成新版本并追加到列表。 使用 synchronized(versions) 对单个 key 的版本列表加锁,粒度小,避免全局锁,提升并发性能。