秒杀项目的总结及面试常见问题

本文介绍了一个秒杀系统的实现,包括登录功能、分布式session管理、秒杀功能的实现、缓存优化(页面静态化、CDN、对象缓存等)、安全措施(密码加密、接口隐藏、验证码、限流)以及面试中常问的问题。通过Redis预减库存、RabbitMQ异步处理、使用内存标记减少Redis访问等方式解决了高并发问题,提升了系统性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目简介

做的是一个秒杀系统,首先是登录系统,采用的是用户的手机号作为用户名,登录的时候先去判断手机号是否存在,然后再判断密码是否正确。当进行密码判断时,为了保证用户的密码的安全性,我这里使用了md5进行两次加密操作,首先通过salt值对输入密码进行第一次加密md5加密从而防止网络传输的过程中密码被截取,然后在到达服务端后在进行第二次md5加密是为了防止数据库的信息泄露时用户信息被反推出来,之后再将客户端输入密码与数据库中的密码相对比。如果密码正确,则通过uuid随机生成一个token标记该用户,并向cookie中写入此token,redis中存储此token与用户信息的映射,当该用户再次登录时,从cookie中取出token,再去redis中取出与此token对应的用户信息。

商品列表这里为了提高性能和减轻对mysql的压力,用到了页面缓存的技术。

接着就是秒杀这个核心功能,首先判断用户是否为空,然后去判断库存是否充足,这里为了解决超卖问题,首先在sql语句上加入了库存大于0的判断,然后还设置了唯一索引来防止一个用户多次秒杀,并且将预库存缓存到了redis当中,当一个秒杀请求到达时判断预库存是否还足够,同时判断是否已经有了该用户的秒杀订单。这里为了优化用户体验,还用上了rabbitmq消息队列,先将该秒杀请求放入消息队列中,以此实现了异步操作就可以让用户不用等待在这里,然后接收端会从消息队列中取出消息进行处理,这里就是真正的去数据库判断库存是否足够了,并且再次判断用户是否已经秒杀过了,然后进行真正的减库存,下订单,生成订单的操作,在消费者处理消息的过程中客户端会自动轮询来判断秒杀的情况。与此同时,考虑到如果对redis的访问中有过多的无效访问的话,资源开销也是较大的,为了解决这个问题,我使用了内存标记来减少对redis的访问,即设置一个map,key是商品,value在系统初始化时设置成false,当预库存不足时设置成true。当秒杀请求到达判断预库存之前会先判断该商品的value是否为true,为true就直接返回秒杀失败。

由于html源码是透明的,为了保护秒杀接口,避免在可以秒杀的时段之外有人通过html源码看到秒杀地址,从而实现秒杀时段外的秒杀, 我还进行了秒杀接口地址的隐藏。每一次执行真正地秒杀时都会通过uuid生成一个随机值,然后和url拼接再放到redis中,当要秒杀时会先验证地址是否合法,然后才会进入真正的秒杀页面,同时我还加上了用户验证码,也就是在获取秒杀地址的时候先通过验证码才能获得,最后为了防刷进行了限流操作,将用户的访问次数保存在了redis中,每访问一次次数减1,我这里设置的是5秒钟内点击5次的限制。

整体的流程


1.登录进入商品列表页面
2.点击进入商品详情页面,静态资源缓存,Ajax获取验证码等动态信息
3.点击秒杀按钮, 将验证码结果和商品ID传给后端,如果结果正确。动态生成随机串UUID,结合用户ID和商品ID(url)存入redis,并将path传给前端。前端获取path后,再根据path地址调用秒杀服务
4.服务端获取请求的path参数,去查缓存是否在。
5.如果存在,并且Redis还有库存,预减redis库存,看是否已经生成订单,没有的话就将请求入消息队列
6.从消息队列的接收端获取消息:包括商品ID和用户ID,判断数据库库存以及是否重复秒杀,然后下单。下单过程是:减库存,下订单,生成订单。
8.生成订单成功或者失败后,都将秒杀结果放到redis中;前端采用ajax轮询方式查询redis获取最终秒杀结果,返回给用户提示。

项目亮点:


1、解决分布式Session问题,服务器水平扩展时,多台服务器都可以响应。

解决方式:分布式session问题,由于水平扩展不同服务器上tomcat容器各自存储session,如果ngnix将访问ServerA的前端请求,发送给ServerB,那么就会查不到对应session,session信息丢失,使得用户重新跳转到登录界面。集中存储到redis服务器上,建立的session信息都可以从缓存中拿到。

如果没有引入分布式session时,因为服务部署在不同服务器上,session存储在本地tomcat服务器上,所以通过ngnix反向传输回来,不一定是传给有seesion的那台服务器,从而导致登录失败。

解决方案:
分布式session实现,是将session集中存储在redis中,验证直接从redis中取。

分布式session基于token实现,生成唯一性的uuid作为key(com.imooc.miaosha.util.UUIDUtil

)序列化用户信息对象存储在redis中

2、多级缓存提高页面访问速度和并发量,减少数据库压力。利用内存标记减少redis的访问。

解决方式:本项目大量的利用了缓存技术,包括用户信息缓存(分布式session),商品信息的缓存,商品库存缓存,订单的缓存,页面缓存。访问速度:内存>redis>数据库sql


3、秒杀令牌对下单接口解耦,并限制令牌个数(创建秒杀大闸),一定程度缓解下单接口访问压力。

解决方式:使用秒杀令牌token,将大量验证信息下单接口分离出来,每个秒杀都需要创建特定的秒杀令牌PromoId_itemId_userId并限定10min存活时间,一定程度防止生成过多token,导致内存占满。限制令牌个数也能够防止恶意访问接口。


4、对库存做内存标记,减少redis压力。只在redis中更新库存,是以乐观锁方式。使用RabbitMq消息队列异步同步数据库,达到一种柔性事务,减少数据库访问压力。并且防止消息发送失败导致数据不一致性,使用事务型消息包裹创建订单操作,保证createorder(1、订单入库;2、落单减库存;3、更新交易日志。)为原子性操作,也保证消息成功投放,即使发送失败也会回滚。

解决方式:(1)库存售罄没必要再去访问redis,直接stock的内存标记。hashmap或者guava。(2)落单减库存是先扣减,不够再回补的一种乐观锁方式。数据库sql采用where stock>0方式加上行锁。并发时会导致QPS很低,性能大幅降低。所以使用消息队列做异步更新,降低数据库压力。(使用CAP理论,BASE理论,考虑可用性和分布式情况,牺牲即刻的一致性)分布式情况下,先扣减redis导致不一致性,但是能达到最终一致性的一种柔性事务。(3)消息发送失败可能导致数据不一致性(redis更新了,数据库没更新成功),需要用事务型消息包裹创建订单操作,保证createorder(1、订单入库;2、落单减库存;3、更新交易日志。)为原子性操作,也保证消息成功投放,即使发送失败也会回滚。但是生成的交易单号要设为Propagation.REQUIRES_NEW防止重复利用订单号。也就是处于大事务中只要完成了就会更新数据。


5、对开启事务型消息这个大事务和初始化订单流水,使用异步化操作,并且使用队列化泄洪。创建线程池,使用阻塞队列存储任务,如果我们希望所有任务都完成可以设置饱和策略为:Caller-Runs让主线程也帮助执行任务。使用Future对象异步接收执行结果。


6、md5密码校验,秒杀接口地址的隐藏,接口限流防刷,下单验证码手动延迟请求。


7、前后端分离,客户端使用token进行其他业务请求,减轻服务端检验用户的压力。

什么是Token?

Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。

Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位


登录功能的实现

一、数据库设计:不做注册,直接登录,在MySQL中直接创建表;用户表包括id、nickname、password、salt、头像、注册时间、上次登录时间、登录次数等字段

二、明文密码两次MD5处理:加密的目的:第一次是因为http是明文传输的,第二次为了防止数据库被盗

三、JSR303参数校验+全局异常处理:通过对输入的参数LoginVo加注解@validated,然后在传入的参数mobile和password上加上注解判断,如@NotNull判断是否为空,也可以自定义局异常处理

四、分布式Session:

分布式session(登录信息缓存)

分布式session的原理:把一个token映射成一个用户,这个过程中分布式session的session并没有存入容器中,而是存到单独的一个缓存中,用一个redis来单独的管理session,这就是所谓的分布式session

核心:将信息存放到一个第三方的缓存当中

分布式session的实现方式:登陆成功之后,给用户生成token来标识这个用户,并写到cookie当中,传递给客户端,然后客户端在随后的访问中都在cookie中上传这个token。服务端拿到这个token之后,利用token取到用户对应的session信息

分布式session问题,由于水平扩展不同服务器上tomcat容器各自存储session,如果ngnix将访问ServerA的前端请求,发送给ServerB,那么就会查不到对应session,session信息丢失,使得用户重新跳转到登录界面。集中存储到redis服务器上,建立的session信息都可以缓存中拿到。

实现秒杀功能

一、数据库设计:包括商品表(goods)、商

<think>嗯,用户现在想要的是面试题问题及答案。之前已经给过他们一个Java架构师面试题的梳理,现在他们可能想要更具体的题目和对应的答案。我需要分析他们的需求,看看他们是不是在准备面试,需要实际的例子来练习。 首先,用户之前的请求是关于Java架构师面试题的介绍,现在则是直接要求“面试题问题及答案”,可能希望得到更详细的问题和解答示例。他们可能已经在准备面试,需要具体的题目来测试自己的知识,或者作为复习材料。 接下来,我需要确保回答的结构清晰,问题分类合理,答案准确。根据之前的回答,用户可能期望类似的分类,比如Java核心、系统设计、数据库等,每个部分都有几个典型问题和答案。 还要注意用户提到的系统级指令,比如使用正确的LaTeX格式,行内公式用$...$,独立公式用$$...$$。需要检查答案中的技术术语是否有正确的格式,例如在讲解Snowflake算法时,确保公式用$$包裹。 另外,用户希望用中文回答,所以所有内容都需要用中文表达,避免术语混淆。同时,答案要真实可靠,可能需要参考常见的面试题和权威解答,避免错误信息。 可能还需要考虑用户是否需要更深入的问题,或者更基础的题目,但根据之前的交互,用户关注的是架构师级别的面试题,所以问题应该偏向高级主题,如分布式系统、微服务、性能优化等。 需要确保每个问题都有简洁但完整的答案,例如在解释CAP理论时,要明确各个原则的含义和实际应用。同时,示例问题解答部分要具体,比如分布式ID生成的三种方法,用LaTeX公式展示Snowflake算法,这样更直观。 最后,检查是否有遗漏的重要主题,比如框架相关的问题(SpringSpring Boot)、设计模式、JVM调优等,确保覆盖Java架构师面试的核心领域。同时,开放性问题如项目经验和技术趋势也不能少,帮助用户全面准备。</think>以下是一些典型的 **Java架构师面试题及参考答案**,涵盖核心知识点和设计场景,帮助您针对性准备: --- ### **一、Java核心与JVM** **1. 如何解决`OutOfMemoryError`问题?** - **分析步骤**: 1. 使用`jstat`或VisualVM监控堆内存变化 2. 通过`-XX:+HeapDumpOnOutOfMemoryError`生成堆转储文件 3. 用MAT工具分析大对象(如内存泄漏的集合类) - **常见原因**:未关闭的数据库连接、静态集合长期持有对象、大文件未流式读取 **2. `synchronized`和`ReentrantLock`的区别?** - **锁机制**:`synchronized`是JVM层面的隐式锁;`ReentrantLock`是API层面的显式锁 - **功能对比**: - `ReentrantLock`支持公平锁、可中断锁、多条件变量 - `synchronized`自动释放锁,代码更简洁 --- ### **二、分布式系统** **1. 如何实现分布式锁?** - **方案1(Redis)**: ```java // 使用SET命令+过期时间(防止死锁) String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); if ("OK".equals(result)) { // 获取锁成功 } ``` - **方案2(ZooKeeper)**:通过创建临时有序节点实现,Watch机制监听节点删除事件 **2. 解释CAP理论并举例** - **定义**: - **C(一致性)**:所有节点数据一致 - **A(可用性)**:每次请求都能获得响应 - **P(分区容忍性)**:网络分区时系统仍能运行 - **取舍案例**: - **CP系统**:ZooKeeper(选举期间拒绝写入) - **AP系统**:Eureka(服务注册表允许短暂不一致) --- ### **三、数据库与缓存** **1. 如何优化慢SQL查询?** - **排查步骤**: 1. 使用`EXPLAIN`分析执行计划,关注`type`(扫描方式)和`Extra`字段 2. 添加索引(避免全表扫描),如联合索引需满足最左匹配原则 3. 优化SQL逻辑(如减少子查询、避免`SELECT *`) **2. 缓存穿透 vs 缓存击穿 vs 缓存雪崩** - **穿透**:查询不存在的数据(解决方案:布隆过滤器拦截非法Key) - **击穿**:热点Key过期后高并发查询压垮DB(解决方案:互斥锁重建缓存) - **雪崩**:大量Key同时过期(解决方案:随机过期时间+二级缓存) --- ### **四、系统设计** **1. 设计一个秒杀系统** - **核心难点**:高并发下的库存超卖与性能瓶颈 - **关键方案**: - **流量削峰**:MQ异步处理请求,客户端轮询结果 - **库存扣减**:Redis预扣库存(Lua脚本保证原子性)+ 数据库最终一致性 - **限流熔断**:Sentinel对突发流量熔断降级 **2. 如何设计API网关的限流?** - **算法选择**: - **令牌桶**:允许突发流量(如Guava的`RateLimiter`) - **漏桶**:恒定速率处理请求 - **实现层级**: - **网关层**:Nginx限流模块或Spring Cloud Gateway的`RequestRateLimiter`过滤器 - **服务层**:AOP切面+Redis计数器(如`INCR`命令) --- ### **五、开放性问题** **1. 如何降低系统耦合度?** - **策略**: 1. 模块化拆分(DDD领域划分) 2. 事件驱动架构(通过MQ解耦服务) 3. 依赖倒置(面向接口编程) **2. 如何管理技术债务?** - **步骤**: 1. 代码扫描工具(如SonarQube)识别高风险代码 2. 制定重构计划(每次迭代预留20%时间处理债务) 3. 建立技术评审机制,防止新增债务 --- ### **六、实战代码题示例** **题目:线程安全的单例模式** ```java public class Singleton { // 双重校验锁+volatile防止指令重排序 private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } ``` --- ### **总结建议** 1. **原理优先**:面试官常追问底层机制(如“ConcurrentHashMap如何保证线程安全?”需答分段锁/CAS) 2. **场景化回答**:结合项目说明技术选型原因(如“为什么选择Kafka而非RabbitMQ?”) 3. **横向对比**:同类技术的优缺点(如Redis Cluster vs Codis) 附:**分布式ID生成公式(Snowflake算法)** $$ \text{ID} = \text{41位时间戳} \ |\ \text{10位机器ID} \ |\ \text{12位序列号} $$ 通过以上问题及答案的梳理,可系统化提升技术深度与表达逻辑。建议结合自身项目经验,针对性准备案例分析。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值