一、问题描述
在并发环境下,多个请求同时对同一条数据库记录进行写操作时,可能会导致数据覆盖问题(即对同一条数据库记录并发写可能会导致一个请求的数据更新被另外一个请求覆盖)。例如:
- 请求A读取库存为100
- 请求B读取库存为100
- 请求A扣减库存10,更新为90
- 请求B扣减库存20,更新为80
- 最终库存为80,而不是预期的70(100-10-20)
本质原因:并发覆写本质是多个请求基于旧值更新,导致后操作覆盖了前操作,解决方案的核心是确保更新操作的原子性或通过锁/版本控制协调并发;
二、解决方案
方案总结
1.增量更新方式
增量更新是指在更新数据时,仅对字段进行增加或减少操作,而不是直接覆盖字段的值。如:
- 库存减少:
stock = stock - 5
- 点赞数增加:
like_count = like_count + 1
2.悲观锁,如使用select for update 把响应的记录行锁住;
3.乐观锁,如使用版本字段,每次更新时检查该版本字段和读取时是否一致,相同则更新;
方案瓶颈
悲观锁:以电商库存系统为例,大促场景下悲观锁方案会带来大量行锁问题,导致库存行的操作性能非常差,直至不断超时,最终卡住后继链路,并且也会存在影响系统吞吐量问题;
乐观锁:在电商大促场景,大流量下会产生大量的事务回滚,最终也会影响到系统吞吐量;
增量更新(推荐方案):虽然是推荐方案,但是需要结合业务进行代码处理不能单独使用;
例如:
- 仅适用于数值型字段,增量更新依赖于字段的 可叠加性(如加减操作),因此仅适用于数值型字段(如库存、计数器)。
-
无法处理复杂业务逻辑,增量更新是 纯数值操作,无法表达复杂的业务条件(如“库存不能低于0”、“更新前需要校验其他字段”),无法满足业务对库存数据的要求,不能单独使用,需要进行业务层面的代码控制,对于订单占用或者订单扣减是否会存在超额占用或者库存为负的情况,从业务逻辑的角度来看增量更新不能保证。insert on duplicate key update其实是数据库层面做的insertOrUpdate操作,本质也不能解决这个问题,常见的做法是通过批量查询缓存,提高查询性能,然后使用where条件如 where inventory-num>=0 and occupy+num<=inventory来从业务上进行控制;
1. 增量更新方案
1.1 原理(为什么能解决并发写覆盖?)
- 原子性:
UPDATE table SET stock = stock - 5 WHERE id = 1执行
数据库操作是原子的(不可拆分),那么即使多个请求同时执行,数据库会按顺序处理,避免中间状态被覆盖。 - 避免覆盖:增量更新不会直接覆盖字段值,而是基于当前值进行计算,确保每个请求的操作都能叠加生效。
通过SQL直接对数据进行加减操作,避免先读后写的操作方式。
1.2 单机环境实现
@Service
public class InventoryService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 直接使用SQL进行库存扣减
* 通过 stock + ? >= 0 确保库存不会出现负数
*/
public boolean updateStock(Long productId, int quantity) {
String sql = "UPDATE inventory SET stock = stock + ? WHERE product_id = ? AND stock + ? >= 0";
int updated = jdbcTemplate.update(sql, -quantity, productId, -quantity);
return updated > 0;
}
}
1.3 分布式环境实现
@Service
public class DistributedInventoryService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RedisTemplate redisTemplate;
/**
* 分布式环境下的库存更新
* 1. 更新数据库
* 2. 删除缓存,采用Cache-Aside模式
*/
public boolean updateStock(Long productId, int quantity) {
// 1. 更新数据库
String sql = "UPDATE inventory SET stock = stock + ? WHERE product_id = ? AND stock + ? >= 0";
int updated = jdbcTemplate.update(sql, -quantity, productId, -quantity);
if (updated > 0) {
// 2. 删除缓存
String cacheKey = "inventory:" + productId;
redisTemplate.delete(cacheKey);
return true;
}
return false;
}
/**
* 查询库存
* 优先从缓存获取,缓存未命中则从数据库查询并更新缓存
*/
public Integer getStock(Long productId) {
String cacheKey = "inventory:" + productId;
// 1. 从缓存获取
Integer stock = (Integer) redisTemplate.opsForValue().get(cacheKey);
if (stock != null) {
return stock;
}
// 2. 从数据库查询
String sql = "SELECT stock FROM inventory WHERE product_id = ?";
stock = jdbcTemplate.queryForObject(sql, Integer.class, productId);
// 3. 更新缓存
if (stock != null) {
redisTemplate.opsForValue().set(cacheKey, stock, 1, TimeUnit.HOURS);
}
return stock;
}
}
2. 悲观锁方案
2.1 单机环境实现
@Service
public class PessimisticLockService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 使用SELECT FOR UPDATE锁定行
* 在事务中执行,确保数据一致性
*/
@Transactional
public boolean updateStock(Long productId, int quantity) {
// 1. 锁定行并查询库存
String selectSql = "SELECT stock FROM inventory WHERE product_id = ? FOR UPDATE";
Integer currentStock = jdbcTemplate.queryForObject(selectSql, Integer.class, productId);
if (currentStock < quantity) {
return false;
}
// 2. 更新库存
String updateSql = "UPDATE inventory SET stock = stock - ? WHERE product_id = ?";
jdbcTemplate.update(updateSql, quantity, productId);
return true;
}
}
2.2 分布式环境实现
根据不同的分布式数据库架构,实现方式有所不同:
2.2.1 单数据库实例/数据库集群场景(主从复制)
@Service
public class SingleDBInventoryService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 使用数据库行锁保证并发安全
* 适用于单数据库实例或数据库集群场景
*/
@Transactional
public boolean updateStock(Long productId, int quantity) {
// 1. 使用行锁查询库存
String selectSql = "SELECT stock FROM inventory WHERE product_id = ? FOR UPDATE";
Integer currentStock = jdbcTemplate.queryForObject(selectSql, Integer.class, productId);
if (currentStock < quantity) {
return false;
}
// 2. 更新库存
String updateSql = "UPDATE inventory SET stock = stock - ? WHERE product_id = ?";
jdbcTemplate.update(updateSql, quantity, productId);
return true;
}
}
2.2.2 分库分表场景
@Service
public class ShardingInventoryService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 使用分布式锁保证并发安全
* 适用于分库分表场景
*/
public boolean updateStock(Long productId, int quantity) {
// 获取分布式锁
RLock lock = redissonClient.getLock("inventory:lock:" + productId);
try {
// 尝试获取分布式锁
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
// 直接更新库存,不需要行锁
String sql = "UPDATE inventory SET stock = stock - ? WHERE product_id = ? AND stock >= ?";
int updated = jdbcTemplate.update(sql, quantity, productId, quantity);
return updated > 0;
} finally {
lock.unlock();
}
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
3. 乐观锁方案
3.1 单机环境实现
@Service
public class OptimisticLockService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 使用版本号实现乐观锁
* 更新时检查版本号是否匹配
*/
public boolean updateStock(Long productId, int quantity, int version) {
String sql = "UPDATE inventory SET stock = stock - ?, version = version + 1 " +
"WHERE product_id = ? AND version = ? AND stock >= ?";
int updated = jdbcTemplate.update(sql, quantity, productId, version, quantity);
return updated > 0;
}
/**
* 查询库存信息(包含版本号)
*/
public StockInfo getStockInfo(Long productId) {
String sql = "SELECT stock, version FROM inventory WHERE product_id = ?";
return jdbcTemplate.queryForObject(sql, (rs, rowNum) ->
new StockInfo(rs.getInt("stock"), rs.getInt("version")), productId);
}
}
3.2 分布式环境实现
根据不同的分布式数据库架构,实现方式有所不同:
3.2.1 单数据库实例/数据库集群场景
@Service
public class SingleDBOptimisticLockService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 使用版本号实现乐观锁
* 适用于单数据库实例或数据库集群场景
*/
public boolean updateStock(Long productId, int quantity, int version) {
String sql = "UPDATE inventory SET stock = stock - ?, version = version + 1 " +
"WHERE product_id = ? AND version = ? AND stock >= ?";
int updated = jdbcTemplate.update(sql, quantity, productId, version, quantity);
return updated > 0;
}
}
3.2.2 分库分表场景
@Service
public class ShardingOptimisticLockService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 分库分表场景下的乐观锁实现
* 使用分布式锁 + 乐观锁
*/
public boolean updateStock(Long productId, int quantity, int version) {
RLock lock = redissonClient.getLock("inventory:lock:" + productId);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
String sql = "UPDATE inventory SET stock = stock - ?, version = version + 1 " +
"WHERE product_id = ? AND version = ? AND stock >= ?";
int updated = jdbcTemplate.update(sql, quantity, productId, version, quantity);
return updated > 0;
} finally {
lock.unlock();
}
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
三、方案对比
1. 增量更新
- 优点:
- 实现简单
- 性能好
- 无需额外的锁机制
- 缺点:
- 只适用于简单的加减操作
- 不支持复杂的业务逻辑
2. 悲观锁
2.1 单数据库实例/数据库集群
- 优点:
- 实现简单
- 保证数据一致性
- 适合写多读少的场景
- 缺点:
- 并发性能较差
- 容易造成死锁
- 锁的粒度较大
2.2 分库分表
- 优点:
- 可以跨数据库实例保证并发安全
- 实现相对简单
- 缺点:
- 依赖分布式锁服务
- 分布式锁可能因为网络问题导致锁失效
- 系统复杂度增加
3. 乐观锁
- 优点:
- 并发性能好
- 不会产生死锁
- 适合读多写少的场景
- 缺点:
- 实现相对复杂
- 需要处理版本冲突
- 可能出现更新失败需要重试的情况