事务管理
- 使用合适的事务隔离级别:
- 在电商场景中,可考虑使用
READ COMMITTED
隔离级别。READ COMMITTED
能保证一个事务只能读取到已提交的数据,在一定程度上避免脏读问题,同时相较于SERIALIZABLE
等更严格的隔离级别,锁的持有时间较短,能减少锁等待时间。例如,在MySQL中,默认的事务隔离级别就是REPEATABLE READ
,可以根据业务情况调整为READ COMMITTED
。
- 代码示例(以Java和JDBC为例):
Connection conn = DriverManager.getConnection(url, username, password);
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
try {
conn.setAutoCommit(false);
// 执行检查库存、更新库存和插入订单的SQL语句
conn.commit();
} catch (SQLException e) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
- 缩短事务的执行时间:
- 将事务中的操作尽可能简化,避免在事务内执行复杂的业务逻辑或耗时操作。只在事务内执行检查库存、更新库存和插入订单这些核心操作。例如,不要在事务内进行复杂的价格计算等操作,可在事务外提前计算好。
锁机制
- 行级锁:
- 在数据库层面,使用行级锁来锁定
products
表中具体商品对应的行。例如,在MySQL中,默认情况下对记录的修改操作会自动获取行级锁。这样在高并发时,不同的事务可以同时操作不同商品的库存,而不会相互阻塞。
- 示例SQL:
-- 检查库存并锁定商品行
SELECT stock FROM products WHERE product_id =? FOR UPDATE;
-- 更新库存
UPDATE products SET stock = stock -? WHERE product_id =?;
- 乐观锁:
- 在
products
表中添加一个版本号字段(如version
)。每次更新库存时,检查当前版本号与读取时的版本号是否一致。如果一致,则更新库存并将版本号加1;如果不一致,说明库存已被其他事务修改,需要重新读取库存并尝试更新。
- 示例SQL:
-- 读取库存和版本号
SELECT stock, version FROM products WHERE product_id =?;
-- 更新库存,假设读取到的库存为 @stock,版本号为 @version,要减少的库存为?
UPDATE products SET stock = @stock -?, version = version + 1 WHERE product_id =? AND version = @version;
Product product = productMapper.selectProductWithVersion(productId);
int newStock = product.getStock() - quantity;
int updateCount = productMapper.updateStockWithVersion(productId, newStock, product.getVersion());
if (updateCount == 0) {
// 版本号不一致,重新尝试
// 重新读取库存并执行更新操作
}
缓存应用
- 本地缓存:
- 在应用服务器本地使用缓存,如Guava Cache。在处理订单时,首先从本地缓存中读取商品库存信息。如果缓存中有数据,则直接使用;如果没有,则从数据库读取,并将读取到的库存信息放入本地缓存。这样可以减少对数据库的请求次数。
- 示例代码(以Java和Guava Cache为例):
LoadingCache<Long, Integer> stockCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<Long, Integer>() {
@Override
public Integer load(Long productId) throws Exception {
return productMapper.selectStock(productId);
}
});
int stock = stockCache.get(productId);
- 分布式缓存:
- 使用分布式缓存如Redis。可以将商品库存信息存储在Redis中,并且设置合适的过期时间。在处理订单时,优先从Redis中读取库存信息。对于库存的更新操作,先更新Redis中的库存,然后再异步更新数据库中的库存(需要处理好数据一致性问题,如使用消息队列)。
- 示例代码(以Java和Jedis为例):
Jedis jedis = new Jedis("localhost", 6379);
String stockStr = jedis.get("product:" + productId + ":stock");
int stock = Integer.parseInt(stockStr);
// 更新Redis中的库存
jedis.decrBy("product:" + productId + ":stock", quantity);
// 异步更新数据库库存(可通过消息队列实现)
- 缓存一致性处理:
- 当数据库中的库存发生变化时,要及时更新缓存。可以采用缓存失效策略,即当数据库库存更新成功后,删除对应的缓存记录,下次请求时重新从数据库读取并放入缓存。也可以采用缓存更新策略,直接更新缓存中的库存信息,但需要注意处理缓存更新失败的情况。例如,在使用Redis时,当更新数据库库存成功后,使用
DEL
命令删除对应的Redis键。
其他优化
- 批量操作:
- 如果有多个订单需要处理,可以将对
products
表的操作进行批量处理。例如,将多个订单对应的商品库存检查和更新操作合并为一个SQL语句,减少数据库交互次数。在JDBC中,可以使用addBatch
和executeBatch
方法来实现批量操作。
- 队列处理:
- 使用消息队列(如Kafka、RabbitMQ)将订单请求进行排队处理。这样可以将高并发的订单请求转化为顺序处理,避免同时对
products
表进行大量的并发操作,减少锁竞争。每个订单请求进入队列后,依次从队列中取出进行库存检查、更新库存和插入订单等操作。