Bootstrap

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

项目简介

做的是一个秒杀系统,首先是登录系统,采用的是用户的手机号作为用户名,登录的时候先去判断手机号是否存在,然后再判断密码是否正确。当进行密码判断时,为了保证用户的密码的安全性,我这里使用了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)、商品表订单表(order_goods)、秒杀商品表(miaosha_goods)、秒杀商品订单表(miaosha_order)

二、商品列表页:为了展示秒杀商品的详情需要goods和miaosha_goods中的信息,所以封装一个GoodsVo,包括价格、库存、秒杀起始时间

三、商品详情页(goods_detail.

;