004 秒杀下单

2024-05-03 17:52
文章标签 秒杀 下单 004

本文主要是介绍004 秒杀下单,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 超卖问题
    • 方案一
    • 方案二
    • 方案三
    • aop锁(单机锁)
      • aop锁(单机锁)
        • pom.xml
        • LockAspect.java
        • ServiceLock.java
    • 分布式锁
      • Mysql分布式锁
      • Redis分布式锁
        • ServiceRedisLock.java
        • LockRedisAspect.java
    • 下单性能优化
    • 数据一致性
    • 解决一致性问题
      • 异步同步库存

秒杀下单业务步骤:
1.数据校验(身份信息,token,手机号,是否开始,库存是否充足,是否开启秒杀,是否上架)
2.检查库存,锁定库存
3.扣减库存
4.更新库存
5.实现下单

面临的问题:
1.业务问题:如何在高并发模式下,保证库存不会出现超卖
2.性能问题:如何在高并发模式下,保证下单操作性能
3.数据一致性问题:如何在高并发模式下,保证数据一致性

超卖问题

原因:
在高并发模式下,多线程出现了数据脏读,抢占cpu资源情况下,出现了数据脏读,从而操作了多下订单,因此出现超卖

超卖:比如10个商品,商品数量为0的时候,下单了100个订单,多下单90个
超卖

如何解决超卖问题:
1.上锁(意味着性能下降,一旦上锁,意味着程序的串行化的执行)
2.原子性操作
3.队列(Queue,Redis,队列)

方案一

给业务进行上锁,让库存扣减变成一个原子的操作,让下单的操作是串行化执行,只有当第一个线程执行结束后,后一个线程才能开始执行,从而控制库存超卖。
注意:在分布式环境下,需要使用分布式锁来控制库存

方案二

利用redis的单线程模式:实现原子性操作,让库存得到控制。Redis服务具备天然的原子性的操作特性,Redis的每一个操作都是一个原子性的操作,因此可以利用Redis的这个特性,实现库存控制,且Redis是高性能的内存数据库,利用redis实现性能与业务的完美结合
原子性操作
以上数据存储特点:把库存数据进行单独的存储,扣减库存直接使用库存进行扣减,而不是使用商品中数据进行扣减(因为使用商品数据扣减,必然会经过2步操作,这2步不是原子性,除非使用lua)
此时扣减库存的方式
1.扣减库存:hincrement(“seckill_goods_stock_1”,-1) #此操作是一个原子操作,下一个线程看见的是上一个线程执行的结果,线程之间具有先后顺序
2.判断库存是否存在
优点:既兼顾了性能问题,又解决了业务库存超卖问题

方案三

队列:Redis队列(其他队列)都具有原子性的操作:Redis-list队列实现库存超卖解决方案
特点:
1.队列的长度等于库存数量
2.队列中存储的数据是此商品的id
3.每一个商品都对应一个队列
此时扣减库存,只需要pop一个队列的元素即可,因为队列的长度等于库存数量,因此pop元素相当于扣减库存;此操作也是原子操作

aop锁(单机锁)

超卖:比如模拟1000个用户,产生1000个订单,实际上被卖出75个商品,因此超卖925个订单!!!
超卖
使用Lock锁进行库存控制:Lock lock = new ReentrantLock(true);

//开始加锁
lock.lock();finally{
//释放锁
lock.unlock;
}

以上加锁方式不能控制库存
锁事务冲突
锁事务冲突

aop锁(单机锁)

问题:针对以上的锁,事务冲突的问题
解决方案:锁上移(在事务开始之前加锁,事务结束后释放锁)
实现方式:表现层加锁,aop增强的方式进行加锁
aop锁

pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
LockAspect.java

package com.example.aop;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;@Component
@Scope
@Aspect
@Order(1)
public class LockAspect {private Lock lock = new ReentrantLock(true);@Pointcut("@annotation(com.example.aop.ServiceLock)")public void lockAspect(){}//增强方法@Around("lockAspect()")public Object around(ProceedingJoinPoint joinPoint){Object obj = null;//上锁lock.lock();try {//执行业务obj = joinPoint.proceed();} catch (Throwable e) {throw new RuntimeException(e);} finally {//释放锁lock.unlock();}return obj;}
}
ServiceLock.java

package com.example.aop;import java.lang.annotation.*;@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {String description() default "";
}

功能测试:没有出现超卖,加锁已经实现了库存的控制

分布式锁

为什么要使用分布式锁:
在JUC单机锁的模式下,只能在单个jvm进程中起作用,但是在集群,分布式部署模式下,无法使用单机锁控制多个jvm进程的并发修改问题,无法实现库存超卖控制
在集群服务,分布式服务模式下,存在多个jvm进程对共享资源并发修改的问题,单机锁无法控制在进程级别的共享资源互斥访问的问题,因此在分布式环境下,必须使用分布式锁
第三方锁

分布式应用原理:保证jvm进程对共享资源的互斥访问,防止jvm进程对共享资源并发修改
应用场景:
1.秒杀场景
2.12306抢票
3.退款

Mysql分布式锁

Mysql实现分布式锁几种方式:
1.乐观锁,悲观锁(这种方式在分布式模式下无法控制库存,单机可以控制)
在多进程模式下,多个事务出现了数据脏读,从而无法控制超卖,虽然加上行锁,但是锁失效后,事务还未提交,此时别的进程事务来读取数据,读到了脏数据
2.单独设计一个表,实现记录锁的操作(加锁:插入一条数据,释放锁:删除一条数据)

Redis分布式锁

ServiceRedisLock.java

package com.example.redis;import java.lang.annotation.*;@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceRedisLock {String description() default "";
}
LockRedisAspect.java

package com.example.redis;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;@Component
@Scope
@Aspect
@Order(1)
public class LockRedisAspect {@Autowiredprivate HttpServletRequest request;@Pointcut("@annotation(com.example.redis.ServiceRedisLock)")public void lockAspect(){}//增强方法@Around("lockAspect()")public Object around(ProceedingJoinPoint joinPoint){Object obj = null;//获取秒杀idString requestURI = request.getRequestURI();String killId = requestURI.substring(requestURI.lastIndexOf("/")-1,requestURI.lastIndexOf("/"));//上锁boolean res = RedissLockUtil.tryLock("seckill_goods_lock_"+killId, TimeUnit.SECONDS,3,10);lock.lock();try {//执行业务if (res){obj = joinPoint.proceed();}} catch (Throwable e) {throw new RuntimeException(e);} finally {//释放锁if (res){RedissLockUtil.unlock("seckill_goods_lock_"+killId);}}return obj;}
}

在分布式下测试,可以控制库存

下单性能优化

优化一:从缓存中查询商品数据,不再从数据库查询
优化二:扣减库存,从缓存中开始扣减库存,不考虑数据一致性问题,只需要考虑数据最终一致性即可
优化三:异步化改造,下单的时候,只需要把订单数据传入到队列即可表示下单成功,后面队列的消费者来异步消费消息,实现下订单操作
下单的写的操作,当并发量比较大的时候,写操作会竞争锁资源,造成数据库性能下降。因此对这块代码进行异步化改造
异步处理:消费者在消费端进行监听,如果发现队列中有数据,立马消费队列中数据,然后处理业务

//判断库存是否还存在,如果不存在,那么就直接返回Integer stockStatus = (Integer) redisTemplate.opsForValue().get(Constants.REDIS_GOODS_END_KEY+killId);//判断
if(stockStatus!=null && stockStatus.equals(HttpStatus.SEC_GOODS_END)){return HttpResult.error("商品已无库存");
}//优化一:从缓存中查询商品数据,不再从数据库查询TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.opsForValue().get("SECKILL_GOODS_STOCK_"+killId);/**
*库存扣减
*/
private boolean reduceStock(Long killId){Long stockNum = redisTemplate.opsForValue.increment("SECKILL_GOODS_STOCK_"+killId,-1);
//扣减成功
if(stockNum > 0){return true;
}else if(stockNum == 0){//最后一次扣减,stock=1,表示此时库存已经售卖完毕//添加标识,表示库存已经扣减完毕redisTemplate.opsForValue().set(Constants.REDIS_GOODS_END_KEY+killId,HttpStatus.SEC_GOODS_END);return true;
}
//扣减失败
return false;
}//第二步优化,从缓存中扣减库存,保证这个操作的原子性
boolean res = this.reduceStock(killId);
//判断库存是否扣减成功
if(!res){return HttpResult.error("下单失败");
}//下单
TbSeckillOrder order = new TbSeckillOrder();
order.setSeckillId(killId);
order.setUserId(userId);
//使用队列,把订单数据入队
Boolean succ = SeckillQueue.getMailQueue().produce(order);
if(!succ){return HttpResuLt.error("秒杀失败");
}
return HttpResult.ok("秒杀成功");
public void run(ApplicationArguments var){new Thread(() -> {LOGGER.info("提醒队列启动成功");//开启一个线程,一直监听bockingQueue队列while(true){try {//进程内队列TbSeckillOrder order = SeckillQueue.getMailQueue().consume();if(order!=null){//从队列中获取订单,执行下单操作seckillService.startAsyncKilled(order);}} catch (InterruptedException e) {e.printStackTrace();}}}).start();
}

数据一致性

CAP定理
C:一致性(数据一致性:牺牲性能为代价)
A:可用性(性能提升,暂时不追求一致性)
P:分区容错性
redis数据
CAP定理要求在软件架构的设计中,不能同时追求一致性,可用性,要么追求强一致性,要么只实现高性能
问题1:从缓存中扣减库存,存在缓存中缓存库存和数据库库存不一致现象
为了性能考虑,牺牲掉一致性,暂时把数据放在缓存中,放弃了一致性的问题,但是最终需要把数据变成一致性的状态。
如何处理?
(1)最终的一致性:支付完成后,同步库存
(2)异步的方式,同步库存

问题2:下单操作,扣减库存操作不是一个原子操作,一旦下单异常失败,本地事务会回滚,但是redis库存已经发生扣减

解决方案:
(1)异常机制对业务补偿
(2)缓存一致性

解决一致性问题

异步同步库存

发生场景,从缓存中扣减库存,但是数据库的库存没有发生任何的变化,因此可以使用异步的方式同步库存。
异步同步库存
引入新的问题,发送消息的操作和本地事务的操作不是一个原子性
半消息机制
消息一致性:为了保证本地消息,本地事务一致性
事务消息
Rocketmq提供的事务消息,解决本地事务和数据库一致性问题,让发送消息的动作和本地事务是原子性的操作

缓存一致性:先操作数据库,再操作redis
缓存一致性
同时操作数据库,缓存的时候,面临数据库和缓存数据一致性的问题,因为本地事务异常,缓存异常都可能造成数据一致性问题,因此解决这类问题的时候,只需要先操作数据库,后操作缓存即可
1.下单操作数据库出现了异常,本地事务回滚,此时缓存没有进行操作,因此数据是一致性的状态
2.下单成功,缓存操作异常,数据库本地事务会回滚,由于缓存没有操作成功,因此数据还是一致状态


@Transactional
@Override
public HttpResult startAsyncKilled(TbSeckillOrder order) {//为了实现缓存,数据库一致性,先操作数据库,后操作缓存order.setCreateTime(new Date());order.setStatus("0");order.setMoney(BigDecimal.ZERO);seckillOrderMapper.insertSelective(order);//第二步优化,从缓存中扣减库存,保证这个操作的原子性boolean res = this.reduceStock(order.getSeckillId());//判断库存是否扣减成功if(!res){return HttpResult.error("下单失败");}return HttpResult.ok();
}

这篇关于004 秒杀下单的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/957311

相关文章

4B参数秒杀GPT-3.5:MiniCPM 3.0惊艳登场!

​ 面壁智能 在 AI 的世界里,总有那么几个时刻让人惊叹不已。面壁智能推出的 MiniCPM 3.0,这个仅有4B参数的"小钢炮",正在以惊人的实力挑战着 GPT-3.5 这个曾经的AI巨人。 MiniCPM 3.0 MiniCPM 3.0 MiniCPM 3.0 目前的主要功能有: 长上下文功能:原生支持 32k 上下文长度,性能完美。我们引入了

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理 秒杀系统是应对高并发、高压力下的典型业务场景,涉及到并发控制、库存管理、事务管理等多个关键技术点。本文将深入剖析秒杀商品业务中常见的几个核心问题,包括 AOP 事务管理、同步锁机制、乐观锁、CAS 操作,以及用户限购策略。通过这些技术的结合,确保秒杀系统在高并发场景下的稳定性和一致性。 1. AOP 代理对象与事务管理 在秒杀商品

004: VTK读入数据---vtkImageData详细说明

VTK医学图像处理---vtkImageData类 目录 VTK医学图像处理---vtkImageData类 简介: 1 Mricro软件的安装和使用 (1) Mricro安装 (2) Mricro转换DICOM为裸数据  2 从硬盘读取数据到vtkImageData 3 vtkImageData转RGB或RGBA格式 4 练习 总结 简介:         对于医

柯桥外语学校|学说俄语:如何用俄语下单秋天的第一杯奶茶!

朋友们,秋天真的来了,相信大家已经喝过了秋天的第一杯奶茶,还记得你喝的是什么吗? 今天我们来学习如何用俄语点饮料,学完这一课,记得在评论区点一杯哦~ 温度: 冰水: ледяная вода 凉水: холодная вода 常温的水: вода комнатной температуры 热水:горячая вода 开水: кипяток/кипяточе

实战篇:基于SSM的秒杀系统之项目介绍

http://www.toutiao.com/i6353119099488305665/?tt_from=mobile_qq&utm_campaign=client_share&app=news_article&utm_source=mobile_qq&iid=6312189567&utm_medium=toutiao_ios

58架构师解读:如何优化秒杀业务的架构?

http://www.toutiao.com/a6327611229364420866/?tt_from=mobile_qq&utm_campaign=client_share&app=explore_article&utm_source=mobile_qq&iid=5840657922&utm_medium=toutiao_ios

redis学习(011 实战:黑马点评:优惠券秒杀:redis实现全局唯一ID)

黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目 总时长 42:48:00 共175P 此文章包含第48p-第p49的内容 文章目录 全局唯一ID编码 全局唯一ID //String did = dao.haveKeyId(“deputybedthing”); 这里的主键并没有自增长 店

社区团购小程序系统源码+界面diy+分销+团长+供应商+拼团+菜谱+秒杀+预售+配送,开启社区营销新模式

社区团购小程序类似美团优选,兴盛优选平台.是一款针对小区居民开发的在线购物平台,旨在为用户提供便捷、实惠的购物体验;同时还提供了“限时抢购”和“优惠券营销”等多种实惠的购物体验,是小区居民们的不二之选。 一、 特点与优势 社区化运营:以社区为单位,居民自发组织或第三方平台提供服务,增强了社区内部的互动与联系。团购模式:通过集体采购实现商品价格优惠,减少居民购物的时间成本和采购成本。便捷

秒杀系统流量削峰这事应该怎么做?为什么要削峰呢?

点击上方“朱小厮的博客”,选择“设为星标” 回复”1024“获取独家整理的学习资料 如果你看过秒杀系统的流量监控图的话,你会发现它是一条直线,就在秒杀开始那一秒是一条很直很直的线,这是因为秒杀请求在时间上高度集中于某一特定的时间点。这样一来,就会导致一个特别高的流量峰值,它对资源的消耗是瞬时的。 但是对秒杀这个场景来说,最终能够抢到商品的人数是固定的,也就是说100人和10000人发起请求

如何设计出骚气的秒杀系统?

点击上方“朱小厮的博客”,选择“设为星标” 后台回复”加群“加入公众号专属技术群 来源:rrd.me/fukGC 前言 秒杀系统相信很多人见过,比如京东或者淘宝的秒杀,小米手机的秒杀,那么秒杀系统的后台是如何实现的呢?我们如何设计一个秒杀系统呢?对于秒杀系统应该考虑哪些问题?如何设计出骚气的秒杀系统?本期我们就来探讨一下这个问题 博客的目录 秒杀系统应该考虑的问题秒杀系统的设计和技术方案系统架