1. 事务+锁 只适合单体应用
(1)MySQL默认事务隔离级别为可重复读,即1个事务多次读取同1条记录,返回的结果相同,但这只限于读操作。如果其它事务修改了记录,1个事务多次读取同1条记录,返回结果不同
(2)扣库存是1个复合操作,可以分解为,读取库存的值,对库存值进行减操作,将新增写回数据库
(3)在高并发下,如果不加锁,复合操作不能保证原子性。n个线程执行扣库存操作,线程安全情况下,库存值被-n;但如果不加锁,库存值被减的次数肯定<n
(4)例如线程A,查询DB,得到库存值100,将100-1,再写回DB。在未写入DB时,线程B查询DB,得到的库存值也是100,将100-1,再写回DB。此时库存值应该是98,但实际却是99,丢失1次操作
缺点: 使用JVM锁保证线程安全,只适合单体应用,在分布式下,无法保证
下单即减库存,不会出现超卖,但会出现少卖。对于下单的未付款的订单,超时将库存加回来(定时任务)
2. 事务+分布式锁 适合分布式应用,但是性能差,不使用
分布式系统下,每个应用多实例部署,相互独立,部署在单独的Web容器上,无法使用JVM锁保证线程安全。但是可以使用分布式锁,每个应用在扣库存时,去请求分布式锁,请求到的应用对库存进行减操作
缺点: 每次减库存,都需要请求分布式锁,走1次网络,性能差
3. 分库分表,将库存分为多份,由多个实例的数据库持有。库存总量单独存放在分布式系统的节点上。让多个实例轮询处理请求(使用Ribbon的负载均衡机制;传统方式可以通过对userID进行取余或Hash,将请求路由到对应的数据库和表),也就是轮询的扣库存,当库存扣完时,向库存总量申请,如果还有库存,可以继续扣库存,否则库存为0,不能再卖
(1)减少请求分布式锁的次数
实例扣完库存后,向库存总量申请,使用分布式锁做同步。与每1次扣库存都申请分布式锁相比,减少了请求锁的次数,不用每次都走网络。例如,10000个库存,分成10个实例,每个实例持有500个,扣完时向库存总量申请,最多会请求20次锁;但每次都请求分布式锁,需要请求10000次
(2)根据业务并发数,合理设置超时时间
Feign使用Ribbon和Hystrix,Ribbon和Hystrix都有超时时间。Hystrix默认1s,如果超时会执行fallback;Ribbon的connect超时和read超时默认都是1s
对于1000个线程的并发,经过测试,设置Ribbon read超时为3s, connect超时使用默认1s
高版本Feign默认关闭了重试,会和Ribbon重试冲突
(1)设置Ribbon的read超时时间和connect超时时间;设置1台Server的重试次数,设置最大重试的Server数;设置重试策略
默认情况,GET请求 read超时/connect超时,都会重试; 非GET请求,只对connect超时进行重试
(2)启用Feign的Hystrix,超时时间根据Ribbon重试策略设置;不要出现Ribbon还在重试,而Hystrix超时的情况
设置连接数
show variables like 'max_connections';
set GLOBAL max_connections=1000;
service层事务+synchronized,导致线程不安全
锁加在service层事务方法上,多个线程执行时,1个线程获取锁,其它线程等待。当1个线程执行完,退出同步块时,事务还未提交;MySQL默认事务隔离级别,是可重复读,SELECT结果是事务开始时间点的状态,当此时其它线程执行读取操作,读取的值是旧值,导致2个线程对同1个旧值执行操作,丢失1次操作
解决方式: 将synchronized加在Controller方法中
4. 只使用Redis
-
Lua脚本,库存>0时,扣库存;库存=0时,不再操作
-
应用Redis单线程机制
-
Redis崩溃,要有应对方案
最简单的,使用AOF,不使用RDB
网友评论