高并发之问题描述

某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。

高并发应对方案

电商中常见的高并发设计案例

  • 使用缓存,如redis,缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。
  • 悲观锁、乐观锁+redis、队列

最优方案

乐观锁思路:观锁,大多是基于数据版本 Version 记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version”字段来实现。读取出数据时,将此版本号一同读出并存储在redis中,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新并更新数据库表和redis的版本号,否则认为是过期数据并重新循环读取redis中的版本号继续操作。

1
2
3
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};

悲观锁会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁(如 redis 排他锁),在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/**
 * redis排他锁:拒绝并发相同的请求
 * @param string $key
 * @param $value
 * @param int $ttl
 * @return bool
 */
public static function setNxWithTTL(string $key, $value, int $ttl = 300): bool
{
    if ((Redis::connection('default'))->set($key, $value, 'ex', $ttl, 'nx')) {
        return true;
    }
    return false;
}

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。

接口限流算法:漏桶算法&令牌桶算法

  • 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
  • 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理(常用的限流算法有令牌桶算法和漏桶算法)

数据库

  1. 新建数据库表时,如果考虑到高并发操作的情况,那么总是变化的字段一定不能存在于同一张表同一条记录同一个字段内
  2. 数据库考虑表锁功能: 研究基于锁的高并发=》增删改操作数据库会自动加锁
  3. 设定事务隔离级别

后端

  1. 判断用户是否使用同一账号,不同设备,同时操作的情况
  2. 多次判断是否冲突,进行事务回滚,保证数据的一致性
  3. 多考虑字段数据自增情况,而不是直接赋值,这样可以减少并发出现的问题