程序猿眼里的高并发

高并发是指在同一个时间点,有很多用户同时的访问 URL 地址,比如:淘宝的双 11,双 12,就会产生高并发, 如贴吧的爆吧,就是恶意的高并发请求,也就是 DDOS 攻击,再屌丝点的说法就像玩撸啊撸被 ADC 暴击了一样, 那伤害你懂得 ( 如果你看懂了,这个说法说明是正在奔向人生巅峰的屌丝。

高并发会来带的后果

  • 服务端:
    导致站点服务器 /DB 服务器资源被占满崩溃,数据的存储和更新结果和理想的设计是不一样的,比如:出现重复的数据记录,多次添加了用户积分等。
  • 用户角度:
    尼玛,这么卡,老子来参加活动的,刷新了还是这样,垃圾网站,再也不来了。
  • 我的经历:
    在做公司产品网站的过程中,经常会有这样的需求,比如什么搞个活动专题,抽奖,签到,搞个积分竞拍等等,如果没有考虑到高并发下的数据处理,那就 Game Over 了,很容易导致抽奖被多抽走,签到会发现一个用户有多条记录,签到一次获得了获得了多积分,等等,各种超出正常逻辑的现象,这就是做产品网站必须考虑的问题,因为这些都是面向大量用户的,而不是像做 ERP 管理系统,OA 系统那样,只是面向员工。

下面我进行实例分析,简单粗暴,动态分析,纯属本人个人经验分享,如有说错,或者有更好的建议或者意见的请留言,大家一起成长。

并发下的数据处理:

通过表设计, 如:记录表添加唯一约束,数据处理逻辑使用事物防止并发下的数据错乱问题
通过服务端锁进程防止包并发下的数据错乱问题

这里主要讲述的是在并发请求下的数据逻辑处理的接口, 如何保证数据的一致性和完整性,这里的并发可能是大量用户发起的,也可能攻击者通过并发工具发起的并发请求


如例子:通过表设计防止并发导致数据错乱

  • 需求点
    【签到功能】 一天一个用户只能签到一次,
    签到成功后用户获取到一个积分
  • 已知表
    用户表,包含积分字段
    高并发意淫分析 (属于开发前的猜测):
    在高并发的情况下,会导致,一个用户签到记录会有多条,或者用户签到后不止加一积分。
  • 我的设计
    首先根据需求我会添加一张签到记录表,重点来了,这张表需要把用户唯一标识字段 (ID,Token) 和签到日期字段添加为唯一约束,或者唯一索引,这样就可以防止并发的时候插入重复用户的签到记录。然后再程序代码逻辑里,先执行签到数据的添加(这里可以防止并发,添加成功后再进行积分的添加,这样就可以防止重复的添加积分了。最后我还是建议所有的数据操作都写在一个 sql 事务里面, 这样在添加失败,或者编辑用户积分失败的时候可以回滚数据。

如例子 2(事务 + 通过更新锁 防止并发导致数据错乱 或者事物 +Update 的锁表机制)

  • 需求点:
    【抽奖功能】 抽奖一次消耗一个积分 抽奖中奖后编辑剩余奖品总数 剩余奖品总数为 0,或者用户积分为 0 的时候无法进行抽奖
  • 已知表:
    用户表,包含积分字段 奖品表,包含奖品剩余数量字段
  • 高并发意淫分析 (属于开发前的猜测):
    在高并发的情况下,会导致用户参与抽奖的时候积分被扣除,而奖品实际上已经被抽完了
  • 我的设计:
    在事物里,通过 WITH (UPDLOCK) 锁住商品表,或者 Update 表的奖品剩余数量和最后编辑时间字段,来把数据行锁住,然后进行用户积分的消耗,都完成后提交事物,失败就回滚。 这样就可以保证,只有可能存在一个操作在操作这件商品的数量,只有等到这个操作事物提交后,其他的操作这个商品行的事物才会继续执行。

如例子 3(通过程序代码防止包并发下的数据错乱问题)

  • 需求点:
    【缓存数据到 cache 里】, 当缓存不存在的时候,从数据库中获取并保存在 cache 里,如果存在从 cache 里获取,每天 10 点必须更新一次,其他时间点缓存两个小时更新一次 到 10 点的时候,凡是打开页面的用户会自动刷新页面
  • 问题点:
    这里有个逻辑用户触发缓存的更新,用户刷新页面,当缓存存在的时候,会取到最后一次缓存更新时间,如果当前时间大于十点,并且最后缓存时间是 10 点前,则会从数据库中重新获取数据保存到 cache 中。 还有客户端页面会在 10 点时候用 js 发起页面的刷新,就是因为有这样的逻辑,导致 10 点的时候有很多并发请求同时过来,然后就会导致很多的 sql 查询操作,理想的逻辑是,只有一个请求会去数据库获取,其他都是从缓存中获取数据。(因为这个 sql 查询很耗服务器性能,所以导致在 10 点的时候,突然间数据库服务器压力暴增)
  • 解决问题:
    C# 通过 (锁)lock,在从数据读取到缓存的那段代码前面加上锁,这样在并发的情况下只会有一个请求是从数据库里获取数据,其他都是从缓存中获取。

访问量大的数据统计接口

  • 需求: 用户行为数据统计接口,用来记录商品展示次数,用户通过点击图片,或者链接,或者其他方式进入到商品详情的行为次数
  • 问题点:
    这接口是给前端 ajax 使用,访问量会很大,一页面展示的时候就会有几十件商品的展示,滚动条滚到到页面显示商品的时候就会请求接口进行展示数据的统计,每次翻页又会加载几十件
  • 意淫分析:
    设想如果同时有 1W 个用户同时在线访问页面,一个次拉动滚动条屏幕页面展示 10 件商品,这样就会有 10W 个请求过来,服务端需要把请求数据入库。在实际线上环境可能还会超过这个请求量,如果不经过进行高并发设计处理,服务器分分钟给跪了。
  • 解决问题:
    我们通过 nodejs 写了一个数据处理接口,把统计数据先存到 redis 的 list 里。(使用 nodejs 写接口的好处是,nodejs 使用单线程异步事件机制,高并发处理能力强,不会因为数据逻辑处理问题导致服务器资源被占用而导致服务器宕机) 然后再使用 nodejs 写了一个脚本,脚本功能就是从 redis 里出列数据保存到 mysql 数据库中。这个脚本会一直运行,当 redis 没有数据需要同步到数据库中的时候,sleep,让在进行数据同步操作

高并发的下的服务器压力均衡,合理站点架设,DB 部署

以下我所知道的:

  1. 服务器代理 nginx,做服务器的均衡负载,把压力均衡到多台服务器
  2. 部署集群 mysql 数据库, redis 服务器,或者 mongodb 服务器,把一些常用的查询数据,并且不会经常的变化的数据保存到其他NoSQL DB 服务器中,来减少数据库服务器的压力,加快数据的响应速度。
  3. 数据缓存,Cache
  4. 在高并发接口的设计中可以使用具有高并发能力的编程语言去开发,如:nodejs 做 web 接口
  5. 服务器部署,图片服务器分离,静态文件走 CDN
  6. DBA 数据库的优化查询条件,索引优化
  7. 消息存储机制,将数据添加到信息队列中 (redis list),然后再写工具去入库
  8. 脚本合理控制请求,如,防止用户重复点击导致的 ajax 多余的请求,等等。

并发测试神器推荐

  1. Apache JMeter
  2. Microsoft Web Application Stress Tool
  3. Visual Studio 性能负载

高并发经常会发生在有大活跃用户量,用户高聚集的业务场景中,如:秒杀活动,定时领取红包等。
为了让业务可以流畅的运行并且给用户一个好的交互体验,我们需要根据业务场景预估达到的并发量等因素,来设计适合自己业务场景的高并发处理方案。

在电商相关产品开发的这些年,我有幸的遇到了并发下的各种坑,这一路摸爬滚打过来有着不少的血泪史,这里进行的总结,作为自己的归档记录,同时分享给大家。


服务器架构

业务从发展的初期到逐渐成熟,服务器架构也是从相对单一到集群,再到分布式服务。
一个可以支持高并发的服务少不了好的服务器架构,需要有均衡负载,数据库需要主从集群,NoSQL缓存需要主从集群,静态文件需要上传 cdn,这些都是能让业务程序流畅运行的强大后盾。

服务器这块多是需要运维人员来配合搭建,具体我就不多说了,点到为止。
大致需要用到的服务器架构如下:

  • 服务器
    • 均衡负载 (如:nginx,阿里云 SLB)
    • 资源监控
    • 分布式
  • 数据库
    • 主从分离,集群
    • DBA 表优化,索引优化,等
    • 分布式
  • nosql
    • redis
      • 主从分离,集群
    • mongodb
      • 主从分离,集群
    • memcache
      • 主从分离,集群
  • cdn
    • html
    • css
    • js
    • image

并发测试

高并发相关的业务,需要进行并发的测试,通过大量的数据分析评估出整个架构可以支撑的并发量。

测试高并发可以使用第三方服务器或者自己测试服务器,利用测试工具进行并发请求测试,分析测试数据得到可以支撑并发数量的评估,这个可以作为一个预警参考,俗话说知己自彼百战不殆。

第三方服务:

  • 阿里云性能测试

并发测试工具:

  • Apache JMeter
  • Visual Studio 性能负载测试
  • Microsoft Web Application Stress Tool

实战方案

通用方案

日用户流量大,但是比较分散,偶尔会有用户高聚的情况;

场景: 用户签到,用户中心,用户订单,等
服务器架构图: 

说明:

场景中的这些业务基本是用户进入 APP 后会操作到的,除了活动日 (618, 双 11,等),这些业务的用户量都不会高聚集,同时这些业务相关的表都是大数据表,业务多是查询操作,所以我们需要减少用户直接命中 DB 的查询;优先查询缓存,如果缓存不存在,再进行 DB 查询,将查询结果缓存起来。

更新用户相关缓存需要分布式存储,比如使用用户 ID 进行 hash 分组,把用户分布到不同的缓存中,这样一个缓存集合的总量不会很大,不会影响查询效率。

方案如:

  • 用户签到获取积分
    • 计算出用户分布的 key,redis hash 中查找用户今日签到信息
    • 如果查询到签到信息,返回签到信息
    • 如果没有查询到,DB 查询今日是否签到过,如果有签到过,就把签到信息同步 redis 缓存。
    • 如果 DB 中也没有查询到今日的签到记录,就进行签到逻辑,操作 DB 添加今日签到记录,添加签到积分 (这整个 DB 操作是一个事务)
    • 缓存签到信息到 redis,返回签到信息
    • 注意这里会有并发情况下的逻辑问题,如:一天签到多次,发放多次积分给用户。
    • 我的博文 [大话程序猿眼里的高并发] 有相关的处理方案。
  • 用户订单
    • 这里我们只缓存用户第一页的订单信息,一页 40 条数据,用户一般也只会看第一页的订单数据
    • 用户访问订单列表,如果是第一页读缓存,如果不是读 DB
    • 计算出用户分布的 key,redis hash 中查找用户订单信息
    • 如果查询到用户订单信息,返回订单信息
    • 如果不存在就进行 DB 查询第一页的订单数据,然后缓存 redis,返回订单信息
  • 用户中心
    • 计算出用户分布的 key,redis hash 中查找用户订单信息
    • 如果查询到用户信息,返回用户信息
    • 如果不存在进行用户 DB 查询,然后缓存 redis,返回用户信息
  • 其他业务
    • 上面例子多是针对用户存储缓存,如果是公用的缓存数据需要注意一些问题,如下
    • 注意公用的缓存数据需要考虑并发下的可能会导致大量命中 DB 查询,可以使用管理后台更新缓存,或者 DB 查询的锁住操作。
    • 我的博文 [大话 Redis 进阶] 对更新缓存问题和推荐方案的分享。

以上例子是一个相对简单的高并发架构,并发量不是很高的情况可以很好的支撑,但是随着业务的壮大,用户并发量增加,我们的架构也会进行不断的优化和演变,比如对业务进行服务化,每个服务有自己的并发架构,自己的均衡服务器,分布式数据库,nosql 主从集群,如:用户服务、订单服务;


消息队列

秒杀、秒抢等活动业务,用户在瞬间涌入产生高并发请求

场景:定时领取红包,等
服务器架构图:

说明:

场景中的定时领取是一个高并发的业务,像秒杀活动用户会在到点的时间涌入,DB 瞬间就接受到一记暴击,hold 不住就会宕机,然后影响整个业务;

像这种不是只有查询的操作并且会有高并发的插入或者更新数据的业务,前面提到的通用方案就无法支撑,并发的时候都是直接命中 DB;

设计这块业务的时候就会使用消息队列的,可以将参与用户的信息添加到消息队列中,然后再写个多线程程序去消耗队列,给队列中的用户发放红包;

方案如:

  • 定时领取红包
    • 一般习惯使用 redis 的 list
    • 当用户参与活动,将用户参与信息 push 到队列中
    • 然后写个多线程程序去 pop 数据,进行发放红包的业务
    • 这样可以支持高并发下的用户可以正常的参与活动,并且避免数据库服务器宕机的危险

附加:
通过消息队列可以做很多的服务。
如:定时短信发送服务,使用 sset(sorted set),发送时间戳作为排序依据,短信数据队列根据时间升序,然后写个程序定时循环去读取 sset 队列中的第一条,当前时间是否超过发送时间,如果超过就进行短信发送。


一级缓存

高并发请求连接缓存服务器超出服务器能够接收的请求连接量,部分用户出现建立连接超时无法读取到数据的问题;

因此需要有个方案当高并发时候时候可以减少命中缓存服务器;

这时候就出现了一级缓存的方案,一级缓存就是使用站点服务器缓存去存储数据,注意只存储部分请求量大的数据,并且缓存的数据量要控制,不能过分的使用站点服务器的内存而影响了站点应用程序的正常运行,一级缓存需要设置秒单位的过期时间,具体时间根据业务场景设定,目的是当有高并发请求的时候可以让数据的获取命中到一级缓存,而不用连接缓存 nosql 数据服务器,减少 nosql 数据服务器的压力

比如 APP 首屏商品数据接口,这些数据是公共的不会针对用户自定义,而且这些数据不会频繁的更新,像这种接口的请求量比较大就可以加入一级缓存;

服务器架构图:

合理的规范和使用 nosql 缓存数据库,根据业务拆分缓存数据库的集群,这样基本可以很好支持业务,一级缓存毕竟是使用站点服务器缓存所以还是要善用。


静态化数据

高并发请求数据不变化的情况下如果可以不请求自己的服务器获取数据那就可以减少服务器的资源压力。

对于更新频繁度不高,并且数据允许短时间内的延迟,可以通过数据静态化成 JSON,XML,HTML 等数据文件上传 CDN,在拉取数据的时候优先到 CDN 拉取,如果没有获取到数据再从缓存,数据库中获取,当管理人员操作后台编辑数据再重新生成静态文件上传同步到 CDN,这样在高并发的时候可以使数据的获取命中在 CDN 服务器上。

CDN 节点同步有一定的延迟性,所以找一个靠谱的 CDN 服务器商也很重要


其他方案

  • 对于更新频繁度不高的数据,APP,PC 浏览器,可以缓存数据到本地,然后每次请求接口的时候上传当前缓存数据的版本号,服务端接收到版本号判断版本号与最新数据版本号是否一致,如果不一样就进行最新数据的查询并返回最新数据和最新版本号,如果一样就返回状态码告知数据已经是最新。减少服务器压力:资源、带宽