物品W现在库存剩余1个, 用户P1,P2同时购买.则只有1人能购买成功.(前提是不允许超卖)
秒杀也是类似的情况, 只有1件商品,N个用户同时抢购,只有1人能抢到..
这里不谈秒杀设计,不谈使用队列等使请求串行化,就谈下怎么用锁来保证数据正确.
常见的实现方案有以下几种:
1.代码同步, 例如使用 synchronized ,lock 等同步方法
2.不查询,直接更新 update table set surplus = (surplus - buyQuantity) where id = xx and (surplus - buyQuantity) > 0
3.使用CAS, update table set surplus = aa where id = xx and version = y
4.使用数据库锁, select xx for update
5.使用分布式锁(zookeeper,redis等)
下面就针对这几种方案来分析下;
1.代码同步, 例如使用 synchronized ,lock 等同步方法
面试的时候,我经常会问这个问题,很大一部分人都会回答用这个方案来实现.
伪代码如下:
public synchronized void buy(String productName, Integer buyQuantity) { // 其他校验... // 校验剩余数量 Product product = 从数据库查询出记录; if (product.getSurplus < buyQuantity) { return "库存不足"; } // set新的剩余数量 product.setSurplus(product.getSurplus() - quantity); // 更新数据库 update(product); // 记录日志... // 其他业务...}
嗯.. 看着挺合理的,以前我也是这么干的. 所以现在碰到别人这样回答,我就会在心里默默的想.小伙子你是没踩过这坑啊.
先说下这个方案的前提配置:
1).使用spring 声明式事务管理
2).事务传播机制使用默认的(PROPAGATION_REQUIRED)
3).项目分层为controller-service-dao 3层, 事务管理在service层
这个方案不可行,主要是因为以下几点:
1).synchronized 作用范围是单个jvm实例, 如果做了集群,分布式等,就没用了
2).synchronized是作用在对象实例上的,如果不是单例,则多个实例间不会同步(这个一般用spring管理bean,默认就是单例)
3).单个jvm时,synchronized也不能保证多个数据库事务的隔离性. 这与代码中的事务传播级别,数据库的事务隔离级别,加锁时机等相关.
3-1).先说隔离级别,常用的是 Read Committed 和 Repeatable Read ,另外2种不常用就不说了
3-1-1)RR(Repeatable Read)级别.mysql默认的是RR,事务开启后,不