MST

星途 面试题库

面试题:MySQL高并发环境下避免不必要数据请求的深度剖析

在高并发的电商系统中,有`products`表(包含商品库存等信息),`orders`表(记录订单相关信息)。在处理订单时,需要先检查商品库存是否足够,然后更新库存并插入订单记录。如何在保证数据一致性的前提下,最大程度避免在高并发场景下对`products`表不必要的数据请求,比如减少锁等待时间、避免重复读取库存等。请从事务管理、锁机制、缓存应用等多方面详细阐述解决方案。
36.9万 热度难度
数据库MySQL

知识考点

AI 面试

面试题答案

一键面试

事务管理

  1. 使用合适的事务隔离级别
    • 在电商场景中,可考虑使用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();
}
  1. 缩短事务的执行时间
    • 将事务中的操作尽可能简化,避免在事务内执行复杂的业务逻辑或耗时操作。只在事务内执行检查库存、更新库存和插入订单这些核心操作。例如,不要在事务内进行复杂的价格计算等操作,可在事务外提前计算好。

锁机制

  1. 行级锁
    • 在数据库层面,使用行级锁来锁定products表中具体商品对应的行。例如,在MySQL中,默认情况下对记录的修改操作会自动获取行级锁。这样在高并发时,不同的事务可以同时操作不同商品的库存,而不会相互阻塞。
    • 示例SQL:
-- 检查库存并锁定商品行
SELECT stock FROM products WHERE product_id =? FOR UPDATE;
-- 更新库存
UPDATE products SET stock = stock -? WHERE product_id =?;
  1. 乐观锁
    • 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;
  • 代码示例(以Java和MyBatis为例):
Product product = productMapper.selectProductWithVersion(productId);
int newStock = product.getStock() - quantity;
int updateCount = productMapper.updateStockWithVersion(productId, newStock, product.getVersion());
if (updateCount == 0) {
    // 版本号不一致,重新尝试
    // 重新读取库存并执行更新操作
}

缓存应用

  1. 本地缓存
    • 在应用服务器本地使用缓存,如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);
  1. 分布式缓存
    • 使用分布式缓存如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);
// 异步更新数据库库存(可通过消息队列实现)
  1. 缓存一致性处理
    • 当数据库中的库存发生变化时,要及时更新缓存。可以采用缓存失效策略,即当数据库库存更新成功后,删除对应的缓存记录,下次请求时重新从数据库读取并放入缓存。也可以采用缓存更新策略,直接更新缓存中的库存信息,但需要注意处理缓存更新失败的情况。例如,在使用Redis时,当更新数据库库存成功后,使用DEL命令删除对应的Redis键。

其他优化

  1. 批量操作
    • 如果有多个订单需要处理,可以将对products表的操作进行批量处理。例如,将多个订单对应的商品库存检查和更新操作合并为一个SQL语句,减少数据库交互次数。在JDBC中,可以使用addBatchexecuteBatch方法来实现批量操作。
  2. 队列处理
    • 使用消息队列(如Kafka、RabbitMQ)将订单请求进行排队处理。这样可以将高并发的订单请求转化为顺序处理,避免同时对products表进行大量的并发操作,减少锁竞争。每个订单请求进入队列后,依次从队列中取出进行库存检查、更新库存和插入订单等操作。