秒杀系统优化方案吐血整理

您所在的位置:网站首页 秒杀系统的设计方案 秒杀系统优化方案吐血整理

秒杀系统优化方案吐血整理

2024-06-03 11:35| 来源: 网络整理| 查看: 265

前一段时间好好研究了秒杀的问题,我把里面的问题好好总结了,可以说是比较全面的了,真的是吐血整理了。

由于我先是在word中整理的,格式都整理得比较好,放到博客上格式挺难调,暂时按word的格式来吧,有时间了在好好排版下。

主要需要解决的问题有两个:

高并发对数据库产生的压力竞争状态下如何解决库存的正确减少(超卖问题)

优化的思路:

1) 尽量将请求拦截在系统上游

2)读多写少经量多使用缓存 3) redis缓存 +RabbitMQ+ mysql 批量入库

1.   初始秒杀设计 1.1 业务分析

秒杀系统业务流程如下:

由图可以发现,整个系统其实是针对库存做的系统。用户成功秒杀商品,对于我们系统的操作就是:1.减库存。2.记录用户的购买明细。下面看看我们用户对库存的业务分析:

记录用户的秒杀成功信息,我们需要记录:1.谁购买成功了。2.购买成功的时间/有效期。这些数据组成了用户的秒杀成功信息,也就是用户的购买行为。

为什么我们的系统需要事务?

1.若是用户成功秒杀商品我们记录了其购买明细却没有减库存。导致商品的超卖。

2.减了库存却没有记录用户的购买明细。导致商品的少卖。对于上述两个故障,若是没有事务的支持,损失最大的无疑是我们的用户和商家。在MySQL中,它内置的事务机制,可以准确的帮我们完成减库存和记录用户购买明细的过程。

1.2  难点分析

当用户A秒杀id为10的商品时,此时MySQL需要进行的操作是:

1.开启事务。2.更新商品的库存信息。3.添加用户的购买明细,包括用户秒杀的商品id以及唯一标识用户身份的信息如电话号码等。4.提交事务。

若此时有另一个用户B也在秒杀这件id为10的商品,他就需要等待,等待到用户A成功秒杀到这件商品,然后MySQL成功的提交了事务他才能拿到这个id为10的商品的锁从而进行秒杀,而同一时间是不可能只有用户B在等待,肯定是有很多很多的用户都在等待竞争行级锁。秒杀的难点就在这里,如何高效的处理这些竞争?如何高效的完成事务?

1.3 功能实现

我们只是实现秒杀的一些功能:1.秒杀接口的暴露。2.执行秒杀的操作。3.相关查询,比如说列表查询,详情页查询。我们实现这三个功能即可。

1.4 数据库设计

Seckill秒杀表单

Success_seckill购买明细表

在购买明细表中seckill_id和user_phone是联合主键,当重复秒杀的时候,加入ignore防止报错,只是会返回0,表示重复秒杀。

INSERT ignore INTO success_killed(seckill_id,user_phone,state) VALUES (#{seckillId},#{userPhone},0)

在购买明细表中seckill_id和user_phone是联合主键,当重复秒杀的时候,加入ignore防止报错,只是会返回0,表示重复秒杀。

INSERT ignore INTO success_killed(seckill_id,user_phone,state) VALUES (#{seckillId},#{userPhone},0)

1.5 DAO层设计

秒杀表的DAO:减库存(id,nowtime)、由id查询商品、由偏移量查询商品

购买明细表的DAO:插入购买明细、根据商品id查询明细SucceesKill对象(携带Seckill对象)—mybatis的复合查询

减库存和增加明细的sql

复制代码

UPDATE seckill SET number = number-1 WHERE seckill_id=#{seckillId} AND start_time #{killTime} AND end_time >= #{killTime} AND number > 0; INSERT ignore INTO success_killed(seckill_id,user_phone,state) VALUES (#{seckillId},#{userPhone},0)

复制代码

 

1.6 Service层设计

暴露秒杀地址(接口)DTO

复制代码

public class Exposer { //是否开启秒杀 private boolean exposed; //加密措施 private String md5; private long seckillId; //系统当前时间(毫秒) private long now; //秒杀的开启时间 private long start; //秒杀的结束时间 private long end;}

复制代码

 

封装执行秒杀后的结果:是否秒杀成功

复制代码

public class SeckillExecution { private long seckillId; //秒杀执行结果的状态 private int state; //状态的明文标识 private String stateInfo; //当秒杀成功时,需要传递秒杀成功的对象回去 private SuccessKilled successKilled;}

复制代码

 

秒杀过程

接口暴露:

复制代码

public Exposer exportSeckillUrl(long seckillId) { //缓存优化 Seckill seckill = getById(seckillId); //若是秒杀未开启 Date startTime = seckill.getStartTime(); Date endTime = seckill.getEndTime(); //系统当前时间 Date nowTime = new Date(); if (startTime.getTime() > nowTime.getTime() || endTime.getTime() < nowTime.getTime()) { return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); } //秒杀开启,返回秒杀商品的id、用给接口加密的md5 String md5 = getMD5(seckillId); return new Exposer(true, md5, seckillId); }

复制代码

如果当前时间还没有到秒杀时间或者已经超过秒杀时间,秒杀处于关闭状态,那么返回秒杀的开始时间和结束时间;如果当前时间处在秒杀时间内,返回暴露地址(秒杀商品的id、用给接口加密的md5)

为什么要进行MD5加密?

我们用MD5加密的方式对秒杀地址(seckill_id)进行加密,暴露给前端用户。当用户执行秒杀的时候传递seckill_id和MD5,程序拿着seckill_id根据设置的盐值计算MD5,如果与传递的md5不一致,则表示地址被篡改了。

 

为什么要进行秒杀接口暴露的控制或者说进行秒杀接口的隐藏?

现实中有的用户回通过浏览器插件提前知道秒杀接口,填入参数和地址来实现自动秒杀,这对于其他用户来说是不公平的,我们也不希望看到这种情况。所以我们可以控制让用户在没有到秒杀时间的时候不能获取到秒杀地址,只返回秒杀的开始时间。当到秒杀时间的时候才

返回秒杀地址即seckill_id以及根据seckill_id和salt加密的MD5,前端再次拿着seckill_id和MD5才能执行秒杀。假如用户在秒杀开始前猜测到秒杀地址seckill_id去请求秒杀,也是不会成功的,因为它拿不到需要验证的MD5。这里的MD5相当于是用户进行秒杀的凭证。

 

执行秒杀:

复制代码

//秒杀是否成功,成功: 增加明细,减库存;失败:抛出异常,事务回滚 @Transactional public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException { if (md5 == null || !md5.equals(getMD5(seckillId))) { //秒杀数据被重写了 throw new SeckillException("seckill data rewrite"); } //执行秒杀逻辑:增加购买明细+减库存 Date nowTime = new Date(); try { //先增加明细,然后再执行减库存的操作 int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); //看是否该明细被重复插入,即用户是否重复秒杀 if (insertCount 0;

 2.    如何解决少卖问题—Redis预减成功而DB扣库存失败?

前面的方案中会出现一个少卖的问题。Redis在预减库存的时候,在初始化的时候就放置库存的大小,redis的原子减操作保证了多少库存就会减多少,也就会在消息队列中放多少。

现在考虑两种情况:

1)数据库那边出现非库存原因比如网络等造成减库存失败,而这时redis已经减了。

2)万一一个用户发出多个请求,而且这些请求恰巧比别的请求更早到达服务器,如果库存足够,redis就会减多次,redis提前进入卖空状态,并拒绝。不过这两种情况出现的概率都是非常低的。

两种情况都会出现少卖的问题,实际上也是缓存和数据库出现不一致的问题!

但是我们不是非得解决不一致的问题,本身使用缓存就难以保证强一致性:

在redis中设置库存比真实库存多一些就行。

3.   秒杀过程中怎么保证redis缓存和数据库的一致性?

在其他一般读大于写的场景,一般处理的原则是:缓存只做失效,不做更新。

采用Cache-Aside pattern:

失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

更新:先把数据存到数据库中,成功后,再让缓存失效。

 4.  Redis中的库存如何与DB中的库存保持一致?

Redis中的数量不是库存,它的作用仅仅时候只是为了阻挡多余的请求透传到db,起到一个保护DB的作用。因为秒杀商品的数量是有限的,比如只有10个,让1万个请求去访问DB是没有意义的,因为最多只有10个请求会下单成功,剩余的9990个请求都是无效的,是可以不用去访问db而直接失败的。

因此,这是一个伪问题,我们是不需要保持一致的。

 5.   为什么要隐藏秒杀接口?

html是可以被右键->查看源代码,如果秒杀地址写死在源文件中,是很容易就被恶意用户拿到的,就可以被机器人利用来刷接口,这对于其他用户来说是不公平的,我们也不希望看到这种情况。所以我们可以控制让用户在没有到秒杀时间的时候不能获取到秒杀地址,只返回秒杀的开始时间。

当到秒杀时间的时候才返回秒杀地址即seckill_id以及根据seckill_id和salt加密的MD5,前端再次拿着seckill_id和MD5才能执行秒杀。假如用户在秒杀开始前猜测到秒杀地址seckill_id去请求秒杀,也是不会成功的,因为它拿不到需要验证的MD5。这里的MD5相当于是用户进行秒杀的凭证。

6.   一个秒杀系统,500用户同时登陆访问服务器A,服务器B如何快速利用登录名(假设是电话号码或者邮箱)做其他查询?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3