目录

面试题以及相关面经总结

如何设计一个秒杀系统?

秒杀系统是指电商中的促销活动,商品数量少,参与用户多。秒杀系统的整体优化思路就是尽量将流量挡在前面,因为越往后端,我们的处理逻辑越重,处理能力越弱。

可以将系统分为前后端两个部分来思考

前端部分

前端部分的常见优化思路是:页面静态化 + CDN、请求频率限制。 另外还可以在活动开始前将按钮置灰,防止无效点击产生流量。

页面静态化+CDN

活动页面上绝大部分内容都是固定的,比如:商品描述、图片等。这时候没有必要每次都去请求服务端,而是将这些静态的内容放到 CDN (内容分发网络)上。每次打开页面的时候,直接去请求 CDN 服务器,能极大地减少后端的请求流量,又能提高用户打开页面的时间。

CDN 就是内容分发网络,它由非常多台分布在世界各地的缓存服务器组成。用户请求特定域名的时候,会转发到CDN的DNS域名解析服务器,随后会返回一台离用户地理位置最近的一台 CDN 服务器,从而极大地减少了长途网络传输的时间。

请求频率限制

请求频率限制,指的是根据业务的特点,在前端做一些流量拦截,减少后端服务器的压力。常见的拦截方式有:

  1. 设定一个请求概率,只允许 30% 的概率向后端发送接口请求。
  2. 设定一个请求频率,例如 10 秒钟只能请求 1 次,随后按钮置灰。

为了预防某些用户直接调用后台接口,绕过前端的频率检查。常见的方法是在频率检查时生成一个参数,随后请求后端服务时携带上该参数。没有该参数的请求,都视为非法请求,直接拒绝该请求。


后端优化

一般来说,后端的优化有如下几种方式:

  1. 增加缓存层 + 预热数据
  2. 消息队列异步处理
  3. 限流、熔断、兜底
  4. 业务侧优化

增加缓存层+预热数据

为了防止请求直接去读取数据库,可以增加缓存层。当我们需要查询数据库之前,我们先去查询缓存,这样可以减少绝大部分的数据库请求,减轻数据库压力。如果在缓存中找不到数据,我们再去请求数据库,随后再将数据缓存到缓存中。

另外,我们在秒杀活动开始之前,可以手动将热点数据加载到缓存中,从而避免秒杀时去请求数据库。

消息队列异步处理

秒杀活动一般涉及抢购、下单、支付、发货等阶段,而抢购与后续的几个阶段是可以异步执行的。当用户抢购成功后,往消息队列中丢入一个消息,随后再由订单系统消费进行下单处理。

限流、熔断、兜底

当并发请求在正常范围内时,我们正常处理请求。当超过设置的限流阈值时,我们则直接拒绝该请求,提示用户抢购失败。

除了限流之外,不同的系统还可以采用熔断、降级的服务治理措施。

熔断指的是请求的错误次数超过阈值时,不再到用后端服务,直接返回失败。同时每隔一定时间放几个请求去重试后端服务,看看是否正常。如果正常则关闭熔断状态,如果失败则继续快速失败。熔断的目的是避免因下游短暂的异常,导致上游不断重试,最终造成下游有太多请求,最终压垮下游系统。

降级指的是当服务失败或异常后,返回指定的默认信息。降级的目的是保证有基本的信息,当下游异常时,与其返回空信息,不如返回一个有业务含义的默认信息,可以提高用户体验。

业务层优化

如果经过上述手段后,后端流量依然很大,可以通过业务侧优化。

例如,12306 购票时间都在同一时刻,这导致同一时刻并发量太大,系统经常支撑不住。后来 12306 将购票周期放长,可以提前 20 天购买火车票。通过业务侧的优化,我们将本来在 1 个小时的抢购分摊到了 20 天,服务器压力会降低很多。



数据库三范式

  • 第一范式

确保数据库字段的原子性。即数据库表中的每一列都是不可分割的原子数据项。

  • 第二范式

在满足第一范式的基础上,还必须满足两个要求:

  1. 一个表必须有一个主键
  2. 非主键列必须完全依赖于主键,而不能只依赖于主键的一部分
  • 第三范式

在满足第二范式的基础上,还必须满足一个要求:非主键列必须直接依赖于主键,不能出现传递依赖。


如何设置事务隔离级别

可以使用如下命令设置当前会话或者全局事务 的隔离级别:

SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL {read uncommitted | read committed | repeatable read | serializable}

查看当前会话的事务隔离级别:

#mysql 8 版本以下
SELECT @@tx_isolation;

# mysql 8 版本及以上
SELECT @@transaction_isolation;

查看全局的事务隔离级别:

#mysql 8 版本以下
SELECT @@global.tx_isolation;

# mysql 8 版本及以上
SELECT @@global.transaction_isolation;

用数组实现固定长度的阻塞队列

阻塞队列使用泛型数组保存元素,通过构造函数指定容量。同时使用一个整数size来记录队列中元素的数量,一个整数head来记录队列头部的索引,一个整数tail来记录队列尾部的索引。使用ReentrantLock来保证线程安全,使用Condition来实现线程的阻塞和唤醒。当队列已满时,调用put方法的线程会被阻塞,并等待notFull条件的信号。当队列为空时,调用take方法的线程会被阻塞,并等待notEmpty条件的信号。

实现代码如下:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BlockingQueue<T> {

    private Object[] array;
    private int size;
    private int head;
    private int tail;

    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();

    public BlockingQueue(int capacity) {
        array = new Object[capacity];
        size = 0;
        head = 0;
        tail = 0;
    }

    public void put(T element) throws InterruptedException {
        lock.lock();
        try {
            while (size == array.length) {
                notFull.await();
            }
            array[tail] = element;
            tail = (tail + 1) % array.length;
            size++;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (size == 0) {
                notEmpty.await();
            }
            T element = (T) array[head];
            head = (head + 1) % array.length;
            size--;
            notFull.signal();
            return element;
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return size;
        } finally {
            lock.unlock();
        }
    }
}