方案一: 代码同步, 使用 synchronized,lock等同步方法
这个方案不太行,主要是因为以下几点:
1) synchronized 作用范围是单个jvm实例, 如果做了集群,分布式等,就没用了,因为无法跨jvm.
2) synchronized是作用在对象实例上的,如果不是单例,则多个实例间不会同步(这个一般用spring管理bean,默认就是单例)
3) 单个jvm时,synchronized也不能保证多个数据库事务的隔离性. 这与代码中的事务传播级别,数据库的事务隔离级别,加锁时机等相关. 先说隔离级别,常用的是 Read Committed 和 Repeatable Read ,另外2种不常用就不说了。mysql默认的是RR,事务开启后,不会读取到其他事务提交的数据。
jvm内部锁不适合用来保证数据库数据一致性,不能跨jvm
方案二:不查询, 直接更新
update table set surplus = (surplus - buyQuantity) where id = xx and (surplus - buyQuantity) > 0
不具备通用性,不能记录操作前后日志。
方案三:使用乐观锁CAS(Compare And Swap)
update table set surplus = aa where id = xx and version = y
使用CAS要注意:失败重试次数,是否需要限制;失败重试对用户是透明的
推荐使用,但是如果数据竞争激烈,则自动重试次数会急剧上升,需要注意。
方案四:使用悲观锁, select x for update
主要控制的是事务串行化,以解决了数据一致性问题.
利用数据库行锁先进性锁定这条记录
SELECT col1... from product where ID =1 for update;
再进行操作
UPDATE product set product_stock = product_stock- 1;
利用排它锁将并行转化为串行操作
该方案可以在用户数和交易数比较小的公司可以采用,具体用户体验性能较差,还会出现
1) 并发量剧增带来的用户体验性能低下
2) 串行设计不合理同样导致死锁问题的发生
3) 一旦并发量大了就会有大量请求阻塞在这里,导致请求超时,进而整个系统雪崩;而且会频繁的去访问数据库,大量占用数据库资源
4) 可能出现超卖的情况
推荐使用,最简单的方案,但是如果事务过大,会有性能问题,操作不当,会有死锁问题。
方案五: 使用分布式锁(zookeeper,redis等)
查询库存 -> 查看redis中是否包含 -> 包含则在redis中扣减库存;不包含则插入数据库进行扣减 并且同步至redis缓存中
1. Redis分布式锁机制
2. 将库存放到redis使用redis的incrby特性来扣减库存。
将库存放到缓存,利用redis的incrby特性来扣减库存,解决了超扣和性能问题。
但是一旦缓存丢失需要考虑恢复方案。比如抽奖系统扣奖品库存的时候,初始库存=总的库存数-已经发放的奖励数,但是如果是异步发奖,需要等到MQ消息消费完了才能重启redis初始化库存,否则也存在库存不一致的问题。
3. 基于redis-lua脚本实现扣减库存的具体实现
我们使用redis的lua脚本来实现扣减库存
由于是分布式环境下所以还需要一个分布式锁来控制只能有一个服务去初始化库存
需要提供一个回调函数,在初始化库存的时候去调用这个函数获取初始化库存
4. 利用redis+异步调用机制处理
redis扣减库存->记录扣减日志
异步调用(可以写一个workerHandler类 executor是多线程的,它的作用更像是一个规划中心。而Worker则只是个搬运工,它自己本身只有一个线程的。或者使用RocketMQ的异步worker处理 )获取日志->同步数据库。
和方案1类似,只是能跨jvm
参考
https://blog.csdn.net/qq315737546/article/details/76850173
https://blog.csdn.net/u014586894/article/details/84343585