秒杀项目详解


在这里插入图片描述

项目使用的是SpringBoot+Mybatis框架,数据库用的是mysql,缓存用的是redis,消息队列用的是RabbitMQ

  • 使用事务处理保证秒杀成功和生成订单的一致性;

  • 使用Redis做页面缓存、预减库存、接口限流等功能,提高系统性能;

  • 使用RabbitMQ消息队列实现异步下单,降低秒杀时对数据库访问的压力;

  • 使用图形验证码答题的方式防止机器人的恶意访问;

  • 使用redis分布式锁和数据库乐观锁防止超卖;

  • 使用Jmeter设置线程组模拟高并发进行测试;

一、秒杀技术难点:

1、极短时间内大量用户访问,短时高并发;

2、读多写少,从数据库中读取数据的远多于写入数据;

3、线程安全问题,竞争的资源有限,不能超卖;

4、恶意请求,防止机器人或者黑客恶意访问;

5、链接暴露,防止有人查看请求地址,秒杀时提前发请求;

6、数据库压力大,每秒上万甚至十几万的QPS直接请求数据库,数据库可能会挂掉。

二、秒杀设计原则:

1、把尽可能多的请求拦截到系统上游;

2、充分利用缓存,减少落到数据库上的无效请求;

3、确保商品不超卖;

4、某些操作异步化,加快接口响应;

5、快速失败机制,没库存或者异常统一当做秒杀失败处理;

6、采取限流策略,防止接口被刷;

7、限制用户在某个时间段内访问秒杀接口的次数;

8、防止同一用户秒杀到多件商品;

三、秒杀要解决的几大问题:

1、高并发请求引起资源争夺导致商品超卖—— Redis分布式锁,数据库乐观锁

2、高并发请求导致应用服务器或数据库崩溃——限流,拆分服务,加集群

3、高并发情况下导致网络拥堵,严重影响用户体验——动静分离,验证码,异步下单

四、总体结构

1、客户端:按钮防止重复点击、验证码答题进行时间片削峰和防止机器人恶意访问;

2、网络层:

  • CDN:缓存静态资源;
  • nginx:负载均衡将请求均匀分发给服务器,限流,静态资源处理;

3、服务层:redis做缓存、预减库存、分布式锁,限流,削峰降级异步下单提高用户体验(RabbitMQ);

4、数据库层:数据库乐观锁(版本号),唯一索引保证消息队列的不重复消费;

五、具体实现

  1、使用事务处理保证秒杀成功和生成订单的一致性:

  事务是一个原子操作,使用事务保证库存减一和生成订单的一致性,库存减一就必须生成订单,两者要么都执行,要么都不执行。

  2、使用Redis做页面缓存:

  把一些不频繁修改的HTML页面(商品详情页面)加载到Redis缓存中,秒杀开始时,直接从Redis中读取HTML页面,如果Redis中没有,再从数据库中读取,并加载到Redis缓存中。

  3、使用Redis预减库存:

  开始秒杀前,提前把商品的库存加载到Redis中,用户点击秒杀按钮后,请求发送到Redis,Redis先判断库存是否大于0,当库存小于0时,直接返回秒杀失败;当库存大于0时,库存减一,预减成功,封装用户ID和商品ID,发送给RabbitMQ。

  4、使用Redis限流:

  为了防止某些用户可能在秒杀开始前,疯狂点击秒杀按钮,以及机器人或者黑客恶意访问,限制用户在某个时间段内访问秒杀接口的次数

  利用计数器法的思想,采用Redis的String数据类型来实现,每个用户ID设置为key,用户访问次数设置为value,每访问一次,value+1,当在某个时间段内,value的值超过了设定的值,就会给前端返回一个消息(操作过于频繁,请稍后再试)。

  同时也利用了Redis中的数据可以设置过期时间这一特点,当用户在某个时间段内访问次数没有超过设置的值,在下一次时间段内,value的值从0开始计算。

  5、使用RabbitMQ消峰:

  数据库每秒处理的请求有上限,高并发场景下不能让请求无限制的同时操作数据库,所以使用RabbitMQ来消峰,RabbitMQ是真正操作数据库的。RabbitMQ收到封装有用户ID和商品ID的消息后,操作数据库,商品库存减一,生成订单,使用事务保证库存减一和生成订单的一致性。

  异步下单,在Redis预减库存成功之后,需要将封装的消息发送给中间件进行处理,发送完之后,会立即给前端返回一个状态码,值为0,表示秒杀正在进行中,前端会一直轮询这个状态码,当RabbitMQ生成订单成功后,会修改这个状态码为1,当前端轮询到状态码为1时,返回秒杀成功。

  6、使用redis分布式锁(sentx)防止超卖:

  判断库存是否大于0和预减库存这两步操作不是原子性的,使用分布式锁把这两个操作放到一起,使得同一时刻,只有一个线程来判断库存量并预减库存,在该线程操作库存时,其他线程不能修改库存量。

  7、使用数据库乐观锁防止超卖:

  给秒杀表加上一个版本号version字段,判断库存量时不会判断version字段,修改库存量时判断version字段和判断库存量时取出来的version是否相同,不相同就说明有人并发修改过了,舍弃这个操作,重新加载。

六、优化

  1、反向代理

  以代理服务器来接受Internet上的连接请求,然后将请求转换给内部网络上的服务器,并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。这样做的好处是保护了真实的服务器

  2、负载均衡

  一台服务器的单位时间内的访问量越大,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃,为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。

  负载均衡是用反向代理的原理实现的。我们可以建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,再让这个中间服器在服务集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器。如此一来,用户的每次访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃得情况。

  3、负载均衡的分配方式

  • 轮询法(默认):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器宕机,能自动剔除;
  • 随机发(权重)weight:指定轮询几率,weight和访问几率成正比,用户后端服务器性能不均的情况,权重越高,再次被访问的概率越大。
  • fair(第三方):按后端服务器的响应时间来分配请求,响应时间短的优先分配。
  • url_hash(第三方):按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。

  4、动静分离:就是做一个Nginx负载均衡,静态资源请求由Nginx处理,而动态资源请求则由tomcat进行处理。

  5、redis预减库存的优化

  当库存小于0时,给redis设置一个标记,直接返回秒杀失败,让后面的请求不再直接访问redis。

七、补充

1、web服务器和应用服务器的区别

共性:都能用来处理HTTP请求;

不同:

  • web服务器通常用于处理静态资源(图片,js文件,css文件等)。常见的有Nginx、apache。
  • 应用服务器既可以处理动态资源,又可以处理静态资源,只是处理静态资源的效率低于web服务器。常见的有tomcat、jboss。

2、常见的限流算法

  • 计数器法

  限流算法中最简单粗暴,也最容易的一种算法,我们可以在开始时设置一个计数器,每次请求,该计数器+1,如果该计数器的值大于某个值并且与第一次请求的时间间隔在某个规定时间内,就说明请求过多,如果该请求与第一次请求的时间间隔大于规定的时间,并且计数器的值在设定值范围内,重置该计数器。

  • 漏桶算法

  水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

  对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输,这时候漏桶算法可能就不合适了。

  • 令牌桶算法—Guava提供了实现类

  令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。当桶满时,新添加的令牌被丢弃或拒绝。

  令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。

  • 令牌按照固定速率放入一个容量大小确定的桶中
  • 桶满时,新添加的令牌被丢弃或拒绝
  • 当一个请求到达时,从桶中删除一个令牌
  • 如果桶中令牌数<1,则请求被丢失或拒绝。
package 其他;

import java.util.concurrent.locks.ReentrantLock;

public class TokenLimiter {
    private static final int Capacity=100;//令牌桶的最大容量
    private static final int RateToken=10;//产生令牌的速率,1秒10个
    private volatile static int TokenCount=0;//当前令牌桶中令牌的数量,默认为0
    private static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) {
       
        new Thread(()->{
            for(;;){
                lock.lock();
                try {
                    Thread.sleep(1);
                    if(TokenCount<Capacity){
                        TokenCount=TokenCount+RateToken;
                        if(TokenCount>100){
                            TokenCount=TokenCount-RateToken;
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }).start();
        for(;;){
            Request request = new Request();
            lock.lock();
            try {
                if(TokenCount>=1){
                    TokenCount=TokenCount-1;
                }else{
                    System.out.println("访问太过频繁");
                }
            } finally {
                lock.unlock();
            }
        }
    }
}
class Request{
    
}

3、 分布式ID

为什么要有分布式ID?

  在复杂分布式系统中,往往需要对大量的数据和信息进行唯一标识,对数据分库分表后需要一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求,此时就需要一个能够生成全局唯一ID的系统。

分布式ID创建的业务需求

  • 全局唯一,不能出现重复的ID号;

  • 单调递增;

  • 分布式ID中最好包含时间戳,这样能够在开发中快速了解这个分布式ID的生成时间。

分布式ID生成系统满足的条件

  • 可用性高:用户发送了一个获取分布式id的请求,服务器要能99.99%的创建成功;

  • 延迟低:创建分布式ID的速度要快;

  • 高QPS:支持高并发,当有大量的请求获取分布式ID时,服务器要顶的住。

4、rabbitmq保证消息不会被重复消费

  保证消息的幂等性,每个消息设置一个全局唯一ID,消费者获取到消息后先根据ID去查询数据库中是否存在该消息,不存在就消费,存在则丢弃。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值