单机版(不考虑库存问题):
单机版(考虑库存问题):
分布式:
库存控制:
下单操作的时候,不进行库存控制,出现同一件商品被售卖多次的现象。也就是我们通常所说的超卖现象。(纠正概念:
超卖不是把商品库存卖成负数,而是同一件商品被卖多次
)
不考虑库存、不考虑超卖、不考虑并发问题,只考虑性能问题。
没有任何锁操作,直接减库存加销量,直接下单(向订单库中直接插入订单数据——以上操作都是直接操作数据库)
private void startSubmitOrder() {
//查询商品信息
seckillGoods.findOneById(商品Id);
//判断商品是否上架、活动是否开始、库存有没有.....
//商品库存减1、销量加1,更新商品表
seckillGoods.update();
//保存订单
order.save();
}
private void startSubmitOrderMultiThread() {
//查询商品信息
seckillGoods.findOneById(商品Id);
//判断商品是否上架、活动是否开始、库存有没有.....
//商品库存减1、销量加1,更新商品表
seckillGoods.update();
//保存订单(多线程方式下单)
new Thread(() -> {
order.save();
}).start();
}
活动开始之前把商品信息放入缓存中,查询商品直接从缓存中获取。
private void startSubmitOrderCache() {
//从缓存中查询商品信息
redisTemplate.get(商品Id);
//判断商品是否上架、活动是否开始、库存有没有.....
//商品库存减1、销量加1,更新商品表
seckillGoods.update();
//保存订单(多线程方式下单)
new Thread(() -> {
order.save();
}).start();
}
10000用户同时下单,理论上库存还剩0,现库存还剩113个,说明程序锁出现超卖现象。
问题:10000个用户同时下单(他们都认为自己买到商品,实际上存在严重问题),只购买了887个商品。
// 定义全局程序锁(ReentrantLock)
private ReentrantLock reentrantLock = new ReentrantLock();
@Transactional(rollbackFor = Exception.class)
private void startSubmitOrderReentrantLock() {
try {
//加锁
reentrantLock.lock();
//从缓存中查询商品信息
redisTemplate.get(商品Id);
//判断商品是否上架、活动是否开始、库存有没有.....
//商品库存减1、销量加1,更新商品表
seckillGoods.update();
//保存订单
order.save();
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁
reentrantLock.unlock();
}
}
10000用户同时下单,理论上库存还剩0,现库存还剩281个,说明程序锁出现超卖现象。
库存无法控制的原因,和Spring事务有关系:
解决方案:
利用Spring切面编程模式,实现AOP锁。
/**
* @Author: LailaiMonkey
* @Description:自定义aop切面注解
* @Date:Created in 2020-12-06 14:19
* @Modified By:
*/
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {
String description() default "";
}
aop拦截器:
/**
* @Author: LailaiMonkey
* @Description:注解拦截
* @Date:Created in 2020-12-06 14:21
* @Modified By:
*/
@Component
@Scope
@Aspect
//order越小越先执行
@Order(1)
public class LockAspect {
private ReentrantLock reentrantLock = new ReentrantLock();
@Around("@annotation(com.monkey.test.sdfsdaf.ServiceLock)")
public Object lockAspect(ProceedingJoinPoint joinPoint) {
//加锁
Object object = null;
try {
reentrantLock.lock();
object = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
} finally {
//解锁
reentrantLock.unlock();
}
return object;
}
}
同普通下单业务逻辑,需要加拦截注解
@Transactional(rollbackFor = Exception.class)
@ServiceLock
private void startSubmitOrder() {
//查询商品信息
seckillGoods.findOneById(商品Id);
//判断商品是否上架、活动是否开始、库存有没有.....
//商品库存减1、销量加1,更新商品表
seckillGoods.update();
//保存订单
order.save();
}
库存已经完美控制住!!!
将下单信息放入队列,后台线程慢慢处理订单业务(订单异步处理)——blockingQueue
下单——数据放入队列——队列消费数据。
定义全局队列:
/**
* @Author: LailaiMonkey
* @Description:定义全局队列
* @Date:Created in 2020-12-06 15:02
* @Modified By:
*/
public class SeckillQueue {
private final static BlockingQueue<OrderModel> BLOCKING_QUEUE = new LinkedBlockingDeque<>();
//不可以实例化
private SeckillQueue() {
}
private static class SingletonHolder {
private static SeckillQueue queue = new SeckillQueue();
}
public static SeckillQueue getQueue() {
return SingletonHolder.queue;
}
/**
* 放入队列
*
* @param model
* @return
*/
public Boolean product(OrderModel model) {
return BLOCKING_QUEUE.offer(model);
}
/**
* 出队列
*
* @param model
* @return
*/
public OrderModel consumer() throws InterruptedException {
return BLOCKING_QUEUE.take();
}
/**
* 获得队列长度
*
* @param model
* @return
*/
public int size() {
return BLOCKING_QUEUE.size();
}
}
队列消费:
/**
* @Author: LailaiMonkey
* @Description:队列消费
* @Date:Created in 2020-12-06 15:09
* @Modified By:
*/
@Component
public class TaskRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
new Thread(() -> {
while (true) {
try {
OrderModel model = SeckillQueue.getQueue().consumer();
if (model != null) {
//商品库存减1、销量加1,更新商品表
seckillGoods.update();
//保存订单
order.save();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}).start();
}
}
下单:
@Transactional(rollbackFor = Exception.class)
@ServiceLock
private void startSubmitOrder() {
//搭建下单模型
OrderModel model = new OrderModel();
model.set.......
//放入队列
Boolean flag = SeckillQueue.getQueue().product(model);
if (flag) {
//通知用户下单成功
} else {
//秒杀失败
}
}
队列大小为100,此时如果队列已经满了,放入队列的动作就会失败,也就是意味着下单失败,此时队列的大小根据后端业务处理能力进行设置。
使用队列作为缓存对象,减轻服务压力,提升服务吞吐能力。
优化:
缺点:
解决方案:
查询商品库存加for update锁定该商品。
@Transactional(rollbackFor = Exception.class)
private void startSubmitOrderBySqlLock() {
//查询商品信息(select .... for update)
seckillGoods.findOneByIdSqlLock(商品Id);
//判断商品是否上架、活动是否开始、库存有没有.....
//商品库存减1、销量加1,更新商品表
seckillGoods.update();
//保存订单
order.save();
}
库存控制没有问题,性能差
在商品表加一个version字段,当多线程(并发)查询修改时候就可以进行版本比对数据修改,防止数据进行脏读。
@Transactional(rollbackFor = Exception.class)
private void startSubmitOrderBySqlLock() {
//查询商品信息
seckillGoods.findOneById(商品Id);
//判断商品是否上架、活动是否开始、库存有没有.....
//商品库存减1、销量加1,更新商品表,判断数据库version和查询version是否一样
//update ..... where version = 查询商品的version
int flag = seckillGoods.update();
//乐观锁更新成功后方可继续操作
if (flag > 0) {
//保存订单
order.save();
} else {
//提示太火爆了
}
}
由于数据库锁进行操作磁盘数据,性能消耗比较高,因此可以使用内存锁提高下单的性能。
@Transactional(rollbackFor = Exception.class)
private void startSubmitOrderReentrantLock() {
boolean result = false;
try {
//使用redisson加锁,尝试等待3s,上锁后20s自动解除
result = redisson.tryLock("seckill_goods_lock" + 商品Id, TimeUnit.SECONDS, 3, 10);
if (result) {
//从缓存中查询商品信息
redisTemplate.get(商品Id);
//判断商品是否上架、活动是否开始、库存有没有.....
//商品库存减1、销量加1,更新商品表
seckillGoods.update();
//保存订单
order.save();
} else {
//提示太火爆
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁
if (result) {
redisson.unlock("seckill_goods_lock" + 商品Id);
}
}
}
redis锁还是会和本地事务出现冲突,但是由于操作reids是远程操作,通过网络io进行,有网络延迟,因此基本不会出现库存控制失败的现象。redis在数据进行操作是异步的,延迟的,因些当数据事务提交后redis才释放,所以这样没问题。
确保100%没有问题需要redis升级为aop锁,把上述aop锁ReentrantLock换成redis的lock即可。
同上,把blockingQueue换成rabbitMq即可。
项目使用分布式部署,数据一致性处理非常困难:
以上问题必须然会发生,所以在处理数据一致性方面必有取舍,强一致性(基于数据库)——CAP(分布式)、Base理论(最终一致性)