赞
踩
该项目模拟了高并发场景的商城系统,它具备用户注册登录,普通商品以及秒杀商品的下单购买功能。为了解决秒杀场景下的高并发问题。该项目使用nginx反向代理以分布式的方式进行云端部署。并采用了多级缓存策略,包括redis缓存,本地热点数据缓存(Guava cache)以及nginx缓存,并使用页面静态化技术将静态资源缓存在CDN中,降低服务器的压力,加快了用户的访问速度。还使用异步消息队列机制对系统的交易性能进行了优化。在秒杀接口使用秒杀令牌来防止脚本对秒杀接口的不断刷新,并使用秒杀大闸以及队列限洪原理进行了流量削峰处理,保证服务器的稳定性。其他接口也设置了接口限流防刷。最后还使用验证码技术不仅起到削峰的作用,还能防止恶意刷访问。最后使用队列泄洪原理去限制了并发流量。
在数据库减库存时加上库存数量判断,库存数量为0时阻止秒杀订单的生成。
数据库加唯一索引,防止用户重复购买
使用Redis缓存预减库存,减少数据库的访问,缓解数据库压力,提升效率。
采用异步消息队列生成订单,增强用户体验。
在系统初始化时,将商品的库存数量加载到Redis缓存中;
接收到秒杀请求时,在Redis中进行预减库存,当Redis中的库存不足时,直接返回秒杀失败,否则继续进行第3步;
将请求放入异步消息队列中,返回正在排队中;
服务端异步队列将请求出队,出队成功的请求可以生成秒杀订单,减少数据库库存,返回秒杀订单详情。
用户在客户端申请秒杀请求后,进行轮询,查看是否秒杀成功,秒杀成功则进入秒杀订单详情,否则秒杀失败,将Redis的库存重新加回。
缺陷:
可能存在Redis缓存中的库存数和数据库中库存数不一致的情况。
要求用户只能下单一次,把用户id和商品id作为联合主键索引存储到数据库中,下次如果再想插入一样用户id和商品id的行,是会报错的。
一个用户只颁发一个秒杀令牌,验证用户是否已经拿到秒杀令牌了。
设置较长的过期时间
采用redis集群的方式提高redis服务器的可靠性
使用互斥锁,只有拿到这把互斥锁的线程可以进数据库请求数据,其他线程等待,待该线程查到数据存入缓存后其他线程直接使用缓存中的数据
先更新数据库,再删除缓存
使用分布式事务,失败后将缓存加回去。
秒杀令牌加秒杀大闸限制入口流量。
使用队列泄洪的方案,采用线程池技术做了一个拥塞窗口限制瞬时并发数。
采用验证码技术削峰防刷处理。
通过设备凭证系统判断该用户的可疑度,可疑度高则采用验证码验身。
封IP,nginx中有一个设置,单个IP访问频率和次数多了之后有一个拉黑操作。
使用分布式锁,Redisson客户端实现分布式锁
扩展:分布式锁
简单总结分布式锁的实现:
可以使用redis来做简单的实现,其中key为锁的名字,而value标记该所是否被使用,例如,刚开始value是0,当第一个线程请求共享资源时将value置为1,这时其他请求不能再获取到这把锁,一直等到该线程用完贡献资源,又将value设置为0.
1.SETNX,其含义为SET IF NOT EXIST。如果不存在才设置,这样两个客户端只有只有一个才能操作。
// 加锁
2.SETNX lock_key 1
// 业务逻辑
3.DO THINGS
// 释放锁
4.DEL lock_key
存在死锁问题:1.程序处理业务逻辑异常,没及时释放锁 2.进程挂了,没机会释放锁
设置锁超时时间
SET lock_key 1 EX 10 NX
释放了别人的锁(加锁的时候设置一个唯一标识,通过该标识释放锁),这里需要把判断锁和释放锁逻辑写成一个LUA脚本执行,保证原子性。
使用Redisson,采用自动续期方案来防止锁提前过期
主从复制异步会造成锁丢失情况
使用Redlock算法,只有半数以上的Redis成功完成加锁,且总耗时没有超过锁失效时间才认为分布式锁加锁成功,否则失败。释放锁时也要向所有Redis节点发起释放。为避免之前由于网络原因误判加锁失败的Redis上有残留的锁。
使用api increment增库存,设置增长因子,如果是减则乘-1
缓存雪崩问题,采用redis集群,提高redis的稳定性。使用队列泄洪限制同一时间的数据库访问数量来避免数据库的压力过大。
将静态资源放在cdn服务器上,较小服务器的压力,还可做全界面静态化。
高并发
超卖、重复卖问题
脚本恶意请求
数据库扛不住
加了缓存之后的缓存三大问题(击穿、穿透、雪崩)
使用token+redis解决分布式会话问题
token是服务端生成的一串字符串,作为客户端进行请求的一个令牌,当第一次登录后,服务器生成一个userToken便将此Token返回给客户端,存入cookie中保存,以后客户端只需带上这个userToken前来请求数据即可,无需再次带上用户名和密码。二次登录时,只需要去redis中获取对应token的value,验证用户信息即可。
核心参数:
corePoolSize: 线程池核心线程数最大值
maximumPoolSize: 线程池最大线程数大小
keepAliveTime: 线程池中非核心线程空闲的存活时间大小
**unit:**线程空闲存活时间的单位
workQueue: 存放任务的阻塞队列
threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
handler: 线城池的饱和策略事件,主要有四种类型。
一个任务进来,先判断当前线程池中的核心线程数是否小于corePoolSize。小于的话会直接创建一个核心线程去提交业务。如果核心线程数达到限制,那么接下来的任务会被放入阻塞队列中排队等待执行。当核心线程数达到限制且阻塞队列已满,开始创建非核心线程来执行阻塞队列中的 业务。当线程数达到了maximumPoolSize且阻塞队列已满,那么会采用拒绝策略处理后来的业务。
使用秒杀令牌+秒杀大闸的和拥塞窗口的方式进行限流
使用数学公式验证码的方式进行削峰
做完了分布式扩展之后,发现有时候已经登录过了但是系统仍然会提示去登录,后来经过查资料发现是cookie和session的问题。然后通过设置cookie跨域分享以及利用redis存储token信息得以解决。
1.作为一个集中式缓存数据库,分担数据库的压力
2.分布式会话管理
作为异步下单的中间件,利用队列排队下单缓解数据库的并发压力
CPU密集型业务:N+1
IO密集型业务:2N+1
基础框架在单机模式下tps在200左右
动静分离,分布式扩展和引入redis和本地多级缓存后,tps达到了3000左右
该项目中token根据用户名进行颁发,由于用户名是一样的,我们为用户颁发的token是相同的
使用秒杀大闸和拥塞窗口的模式实现
ThreadPoolExecutor.AbortPolicy:``//丢弃任务并抛RejectedExecutionException异常。
DiscardPolicy:``//丢弃任务,但是不抛出异常。
DiscardOldestPolicy:``//丢弃队列最前面的任务,然后重新提交被拒绝的任务
CallerRunsPolicy:``//由调用线程(提交任务的线程)处理该任务
无效,会从redis中删除
设置为秒杀商品的个数减去核心线程数最合适。
jstat -gc vmid count
jstat -gc 12538 5000 // 表示将12538进程对应的Java进程的GC情况,每5秒打印一次
跟随用户的请求会动态变化,令牌桶机制可以控制每秒生成令牌的个数。
redis中库存减成功后,生成一条消息包含了商品信息、用户信息消息由MQ的生产者生产,经由queue模式发送给消费方,即订单生成的业务模块,在该模块会消费这条消息,根据其中的信息进行订单的生成,以及数据库的修改操作。
在扣减库存的时候加where语句判断stock > 0,如果修改失败就回滚。
update table set stock = stock-1 where prom_id = ? and stock > 0;
前端限制: 一次点击之后按钮置灰几秒钟。给按钮设计一个点击时间,用定时器来记时。
后端限制: 每个用户只能获得一个秒杀令牌,然后存入到Redis缓存中。用户的一个下单请求会先判断用户当前是否已经持有令牌了,用户有令牌的话直接返回 “正在抢购中”。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。