悲观锁(Pessimistic Lock)
很悲观,认为每次拿数据都会被别人修改,所以拿的时候就上锁。
- 假设并发冲突一定会发生
- 操作前先加锁,其他人必须等待
- 是阻塞式、排他锁
典型实现
- 数据库:
select ... for update - Java:
synchronized、ReentrantLock
优点
- 强一致性,冲突多的时候更稳定
- 写多读少场景效率高
缺点
- 锁竞争、上下文切换开销大
- 冲突少的时候浪费性能
乐观锁(Optimistic Lock)
核心思想:很乐观,认为一般不会冲突,只在更新时检查是否被改。
- 假设冲突很少发生
- 读不加锁,更新时才校验
- 是非阻塞、无锁设计
典型实现
-
版本号机制(最常用)
-
加字段
version -
更新时:
sqlupdate table set ..., version = version + 1 where id = ? and version = ? -
影响行数=0 说明冲突,重试/报错
-
-
CAS(Compare And Swap)思想
- 比较旧值 → 相等才更新
- C++ :
std::atomic原子类 - Java:
AtomicInteger原子类
优点
- 无锁,高并发、读多写少极快
- 无死锁风险
缺点
- 冲突多会导致大量重试,CPU 飙升
- 只能保证单个变量原子性(ABA 问题)
简洁总结
- 悲观锁:先上锁,再操作 → 安全但慢
- 乐观锁:不加锁,更新时校验 → 快但要处理冲突
使用场景
- 读多写少、高并发 → 乐观锁
- 写多读少、要求强一致 → 悲观锁
在C++后端开发中,乐观锁和悲观锁的使用场景非常明确,核心是根据并发冲突概率 和业务一致性要求来选择。下面结合C++的具体实现和后端开发的典型场景,给你讲清楚怎么用、用在哪。
悲观锁在C++中的应用场景
悲观锁在C++中主要依赖互斥锁、自旋锁 等同步原语实现,核心用于写操作频繁、冲突概率高、强一致性要求的场景。
1. 实现(C++标准库std::mutex)
cpp
#include <mutex>
#include <thread>
#include <iostream>
// 全局互斥锁(悲观锁核心)
std::mutex mtx;
int shared_data = 0;
// 写操作函数(多线程调用)
void write_data(int value) {
// 加锁(悲观:认为一定会有竞争,先锁再操作)
std::lock_guard<std::mutex> lock(mtx);
shared_data = value;
std::cout << "写入数据:" << shared_data << std::endl;
}
int main() {
std::thread t1(write_data, 10);
std::thread t2(write_data, 20);
t1.join();
t2.join();
return 0;
}
2. 使用场景
(1)多线程操作共享资源(写多读少)
- 场景:网关服务中多线程修改连接池、配置中心多线程更新配置、订单系统多线程修改订单状态。
- 原因:写操作频繁,冲突概率高,悲观锁能保证绝对的线程安全,避免数据错乱。
- 扩展:除了
std::mutex,还会用std::recursive_mutex(可重入锁)、pthread_mutex_t(POSIX锁)。
(2)数据库层面的排他锁
-
场景:金融系统扣减余额、秒杀系统扣减库存(C++程序调用SQL)。
-
实现:C++程序执行
select ... for update语句,锁定数据库行,避免并发更新导致数据不一致。cpp// 伪代码:C++调用MySQL执行悲观锁SQL std::string sql = "SELECT balance FROM user WHERE id = 1 FOR UPDATE"; mysql_query(conn, sql.c_str()); // 扣减余额(此时该行已被锁定,其他线程/进程无法修改) sql = "UPDATE user SET balance = balance - 100 WHERE id = 1"; mysql_query(conn, sql.c_str());
(3)临界区操作(必须原子执行)
- 场景:日志系统多线程写入同一个日志文件、网络服务多线程修改全局连接计数。
- 原因:这类操作不能被打断,悲观锁能保证临界区代码完整执行。
乐观锁在C++中的应用场景
乐观锁在C++中主要依赖CAS(Compare-And-Swap) 实现(C++11起提供原子操作库),核心用于读多写少、冲突概率低、追求高性能的场景。
典型实现(C++原子类/CAS)
cpp
#include <atomic>
#include <thread>
#include <iostream>
// 原子变量(底层基于CAS,乐观锁核心)
std::atomic<int> shared_data = 0;
// 更新数据函数(多线程调用)
void update_data(int value) {
int expected = shared_data.load(); // 读取当前值
// CAS操作(乐观:先读值,更新时检查是否被修改,没修改才更新)
while (!shared_data.compare_exchange_weak(expected, value)) {
// 冲突时重试(expected会自动更新为最新值)
std::cout << "数据已被修改,重试更新..." << std::endl;
}
std::cout << "成功更新数据:" << shared_data << std::endl;
}
int main() {
std::thread t1(update_data, 10);
std::thread t2(update_data, 20);
t1.join();
t2.join();
return 0;
}
2. 使用场景
(1)高并发计数器(读多写少)
- 场景:接口访问量统计、缓存命中率统计、秒杀系统的参与人数计数。
- 原因:读操作远多于写操作,冲突概率低,CAS比互斥锁性能高一个量级(无锁开销)。
- 扩展:C++11的
std::atomic系列(atomic_int、atomic_long)都是乐观锁实现,后端中常用于高性能统计。
(2)数据库版本号控制(C++程序配合DB)
-
场景:电商系统更新商品库存(读多写少,大部分是查询库存,少量扣减)、用户资料修改(大部分是查看,少量编辑)。
-
实现:C++程序通过版本号实现乐观锁,避免数据库锁竞争:
cpp// 伪代码:C++调用MySQL实现乐观锁 // 1. 查询商品信息(含版本号) std::string sql = "SELECT stock, version FROM goods WHERE id = 1"; mysql_query(conn, sql.c_str()); int stock = ...; // 读取库存 int version = ...; // 读取版本号 // 2. 扣减库存(更新时校验版本号) sql = "UPDATE goods SET stock = stock - 1, version = version + 1 " "WHERE id = 1 AND version = " + std::to_string(version); int affected_rows = mysql_affected_rows(conn); if (affected_rows == 0) { // 版本号不匹配,说明有并发更新,重试或返回错误 std::cout << "库存更新冲突,请重试" << std::endl; }
(3)无锁数据结构(高性能组件)
- 场景:网关服务的无锁队列、缓存系统的无锁哈希表、高并发RPC框架的无锁连接池。
- 原因:后端中间件对性能要求极高,无锁数据结构(基于乐观锁CAS)能避免锁竞争,提升吞吐量。
- 示例:C++实现无锁栈(核心是CAS操作节点指针)。
乐观锁与悲观锁的选择准则
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 适用场景 | 写多读少、冲突率高、强一致性 | 读多写少、冲突率低、高性能要求 |
| C++实现 | std::mutex、pthread_mutex | std::atomic(CAS)、版本号 |
| 性能开销 | 锁竞争、上下文切换开销大 | 无锁开销,冲突时重试开销 |
| 典型应用 | 订单修改、余额扣减、日志写入 | 计数器、库存查询更新、无锁队列 |