基于互联网架构演进, 构建秒杀系统
新钛云服已累计为您分享649篇技术干货
系统架构师思考
设计一个秒杀系统需要考虑的因素很多,比如对现有业务的影响、网络带宽消耗以及超卖等因素。本文会讨论秒杀系统的各个环节可能存在的问题以及解决方案。
四大核心课题思考
高性能架构
以用户为中心,提供快速的网页访问体验。主要参数有较短的响应时间、较大的并发处理能力、较高的吞吐量与稳定的性能参数。
可分为前端优化、应用层优化、代码层优化与存储层优化。
可伸缩架构
可扩展架构
· 模块化、组件化:高内聚,低耦合,提高复用性,扩展性
· 稳定接口:定义稳定的接口,在接口不变的情况下,内部结构可以“随意”变化
· 设计模式:应用面向对象思想,原则,使用设计模式,进行代码层面的设计
· 消息队列:模块化的系统,通过消息队列进行交互,使模块之间的依赖解耦
· 分布式服务:公用模块服务化,提供其他系统使用,提高可重用性,扩展性
安全架构
· 基础设施安全:
硬件采购,操作系统,网络环境方面的安全。一般采用正规渠道购买高质量的产品,选择安全的操作系统,及时修补漏洞,安装杀毒软件防火墙。防范病毒,后门。设置防火墙策略,建立DDOS防御系统,使用攻击检测系统,进行子网隔离等手段。
· 应用系统安全:
在程序开发时,对已知常用问题,使用正确的方式,在代码层面解决掉。防止跨站脚本攻击(XSS),注入攻击,跨站请求伪造(CSRF),错误信息,HTML注释,文件上传,路径遍历等。还可以使用Web应用防火墙(比如:ModSecurity),进行安全漏洞扫描等措施,加强应用级别的安全。
· 数据保密安全:
存储安全(存储在可靠的设备,实时,定时备份),保存安全(重要的信息加密保存,选择合适的人员复杂保存和检测等),传输安全(防止数据窃取和数据篡改)。
常用的加解密算法(单项散列加密[MD5、SHA],对称加密[DES、3DES、RC]),非对称加密[RSA]等。
一、互联网架构演进思考
1.1 架构演进
1.2 单体架构
· 应用程序 MYSQL分离部署
· 服务集群– 提升性能
· 动态分离(静态资源存储CDN,nginx服务器)
· 隔离术(线程池隔离,进程隔离)
· 队列术 (blockingQueue,disruptor队列,RocketMQ)
· 接入层限流(openresty), 接口限流
· MySQL优化(索引,缓存,表结构,分表分库,数据归档,冷热,SQL语句优化)
· 引入lvs (linux virtual server)
· DNS 解决上层流量瓶颈问题
· 多级缓存
①传统项目(并发量小,业务简单,需求固定),项目体量比较小
②小程序
③追求极致性能的项目(业务量少)
④互联网项目(中小型企业,创业公司)
用户行为:
①产生的时间段:11:00 – 2:00 5:00 – 12:00 ,订单产生时间段:12h
②每下一单会发生多少个请求:50QPS x 3 = 150 QPS
计算平均每一秒QPS:
(4)单点架构优缺点
单体架构优点:
①部署简单
②开发简单
③测试简单
④集群简单
⑤RT响应时间非常快速 —— 适合一些特点的项目(极端苛刻响应时间)
①流量比较集中,所有的请求都集中一个服务中,单体无法应对
②无法实现敏捷开发,业务增大,代码结构越来越臃肿,维护变得非常困难单体架构:war > 1G --- IBM unix 高性能服务器 64cpus, 128GB --- 1GB
③单体架构牵一发而动全身
④扩展性差
⑤稳定性差
1.3 架构拆分
1.4 微服务架构
1.5 ServiceMesh架构
①服务性能监控(Zabbix,promutheus)2、服务限流(sentinel)
②服务降级(sentinel)
③服务熔断(sentinel)
④链路追踪(skywalking)
⑥日志监控(elk)
⑦服务告警
⑧负载均衡
1.6 Serverless
①不需要上传到哪一个服务器
二、性能调优思考-JVM
2.1 JVM的调优思考
思考题1
项目上线后,是什么原因促使必须进行jvm调优?
答案:调优的目的就是提升服务性能。
调优:及时释放内存
调优:防止频繁gc
(3)垃圾回收导致stw(stop the world)
调优:尽可能的减少gc次数
思考题2
jvm调优本质是什么?
答案:jvm调优的本质就是(对内存的调优) 及时回收垃圾对象,释放内存空间;让程序性能得以提升,让其他业务线程可以获得更多内存空间;
思考题3
是否可以把JVM内存空设置的足够大(无限大),是不是就不需要垃圾回收呢?
前提条件:内存空间被装满了以后,才会触发垃圾回收器来回收垃圾;
答案:理论上是的,现实情况不行的!
32位操作系统 === 4GB 内存
64位操作系统 === 16384 PB 内存空间
· 问题1:考虑到寻址速度的问题,寻址一个对象消耗的时间比较长的
· 问题2:一旦触发垃圾回收,将会是一个灾难;(只能重启服务器)
2.2 JVM的调优原则
(2) gc的次数足够少 (jvm堆内存设置的足够大)
(3) 发生fullgc 周期足够长 (最好不发生full gc)
· metaspace 永久代空间设置大小合理,metaspace一旦扩容,就会发生fullgc
· 老年代空间设置一个合理的大小,防止full gc
· 尽量让垃圾对象在年轻代被回收(90%)
· 尽量防止大对象的产生,一旦大对象多了以后,就可能发生full gc ,甚至oom
2.3 JVM的调优原理
怎么找垃圾?
(1)引用计数算法 找垃圾
(2)根可达算法 找垃圾 hotspot 垃圾回收器都是使用这个算法
(1)引用计数算法
如何清除垃圾?
①mark-sweep 标记清楚算法
②copying 拷贝算法
③mark-compact 标记整理(压缩)算法
①使用根可达算法找到垃圾对象,对垃圾对象进标记 (做一个标记)
②对标记对象进行删除(清除)
②第二种算法:copying拷贝算法
①选择(寻址)存活对象
②把存活对象拷贝到另一半空闲空间中,且是连续的内存空间
③把存储对象拷贝结束后,另一半空间中全是垃圾,直接清除另一半空间即可
优点:简单,内存空间是连续的,不存在内存空间碎片
①选择(寻址)存活对象
②把存活对象拷贝到另一半空闲空间中,且是连续的内存空间
③把存储对象拷贝结束后,另一半空间中全是垃圾,直接清除另一半空间即可
特点:
1、Serial Serial Old , parNew CMS , Parallel Scavenge Parallel Old 都属于物理分代垃圾回收器;年轻代,老年代分别使用不同的垃圾回收器;
2、G1 在逻辑上进行分代的,进行在使用上非常方便,关于年轻代,老年代只需要使用一个垃圾回收器即可;
3、ZGC ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器
4、Shenandoah OpenJDK 垃圾回收器
5、Epsilon 是Debug使用的,调试环境下:验证jvm内存参数设置的可行性
6、Serial Serial Old:串行化的垃圾回收器
7、parNew CMS :并行,并发的垃圾回收器
8、Parallel Scavenge Parallel Old :并行的垃圾回收器
1、Serial + Serial Old: 串行化的垃圾回收器,适合单核心的cpu的服务情况
2、parNew + CMS:响应时间优先组合
3、Parallel Scavenge + Parallel Old :吞吐量优先组合
4、g1 :逻辑上分代的垃圾回收器组合
2.4 垃圾回收器原理
注意特点:
1、stw : 当进行gc的时候,整个业务线程都会被停止,如果stw时间过长,或者stw发生次数过多,都会影响程序的性能
2、垃圾回收器线程:多线程,单线程,并发,并行
Parallel Scavenge + Parallel Old
1、stw : 当进行gc的时候,整个业务线程都会被停止,如果stw时间过长,或者stw发生次数过多,都会影响程序的性能
2、垃圾回收器线程:多线程,单线程,并发,并行
parNew+CMS
2.5 内存分代模型
①JDK1.5:68% ,当eden区域装对象达到68%时候,就会触发垃圾回收
②JDK1.6+ : 92%才会触发垃圾回收器
2.6 JVM的实战调优
1、JVM调优本质就是 gc , 垃圾回收,及时释放内存空间
2、gc次数要少,gc时间少,防止fulllgc --- 内存参数设置
1、-Xmx4000m 设置JVM最大堆内存(经验值:3500m – 4000m,内存设置大小,没有一个固定的值,根据业务实际情况来进行设置的,根据压力测试,根据性能反馈情况,去做参数调试)
2、-Xms4000m 设置JVM堆内存初始化的值,一般情况下,初始化的值和最大堆内存值必须一致,防止内存抖动;
3、-Xmn2g 设置年轻代内存对象(eden,s1,s2)
4、-Xss256k 设置线程栈大小,JDK1.5+版本线程栈默认是1MB, 相同的内存情况下,线程堆栈越小,操作系统创建的线程越多;
nohup java -Xmx4000m -Xms4000m -Xmn2g -Xss256k -jar jshop-web-1.0-SNAPSHOT.jar --spring.config.addition-location=application.yaml > jshop.log 2>&1 &
2.7 GC的日志输出
nohup java -Xmx4000m -Xms4000m -Xmn2g -Xss256k -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.config.addition-location=application.yaml > jshop.log 2>&1 &
-XX:+PrintGCDetails 打印GC详细信息
-XX:+PrintGCTimeStamps 打印GC时间信息
-XX:+PrintGCDateStamps 打印GC日期的信息
-XX:+PrintHeapAtGC 打印GC堆内存信息
-Xloggc:gc.log 把gc信息输出gc.log文件中
1、程序样本数不够
2、程序运行的时间不够
3、业务场景不符合要求(查询没有太多的对象数据)
Yong &old比例
①定义年轻代:-Xmn1500m,剩下的空间就是老年代的空间
②参数:-XX:NewRatio = 4 表示年轻代(eden ,s0,s1) 和老年代区域所占比值 1:4
nohup java -Xmx4000m -Xms4000m -Xmn1500m -Xss256k -XX:MetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.config.addition-location=application.yaml > jshop.log 2>&1 &
1、gc消耗时间 –业务时间占比
2、频繁发生fullgc – 调优 – stw—程序暂停时间比较长,阻塞,导致整个程序崩溃
3、oom --- 调优
2.8 GC组合
nohup java -Xmx4000m -Xms4000m -Xmn1500m -Xss256k -XX:MetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.config.addition-location=application.yaml > jshop.log 2>&1 &
nohup java -Xmx4000m -Xms4000m -Xmn1500m -Xss256k -XX:MetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.config.addition-location=application.yaml > jshop.log 2>&1 &
nohup java -Xmx4000m -Xms4000m -Xmn1500m -Xss256k -XX:MetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.config.addition-location=application.yaml > jshop.log 2>&1 &
三、数据库连接池调优思考
3.1 数据库调优动机何在
· Timeout 5xx 错误
· 慢查询导致页面无法加载
· 阻塞导致数据无法提交
很多的数据库问题,都是由于低效的SQL语句造成的(写SQL语句)
· 流畅的业务访问体验
· 良好的网站功能体验
3.2 影响数据库性能的因素
1、低效的SQL语句
2、并发cpu问题(SQL语句不支持多核心的cpu并发计算,也就是说一个SQL只能在一个cpu执行结束)3、连接数:max_connections
4、超高cpu使用率
5、磁盘io性能问题6、大表(字段多,数据多)
7、大事务
1、对项目架构、业务,缓存各方面进行优化,真正数据库请求比较少—减少数据库压力
2、数据库设计,架构,优化
3.3 连接池对性能样例分析(详细IP隐藏)
datasource:
#url: jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
url: jdbc:mysql://XX.XX.XX.XX:3306/shop?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&connectionTimeout=3000&socketTimeout=1200
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
druid:
#配置初始化大小、最小、最大
initial-size: 1
min-idle: 5
max-active:10
max-wait: 10000
time-between-eviction-runs-millis: 600000
# 配置一个连接在池中最大空闲时间,单位是毫秒
min-evictable-idle-time-millis:300000
# 设置从连接池获取连接时是否检查连接有效性,true时,每次都检查;false时,不检查
test-on-borrow: true
#设置往连接池归还连接时是否检查连接有效性,true时,每次都检查;false时,不检查
test-on-return: true
# 设置从连接池获取连接时是否检查连接有效性,true时,如果连接空闲时间超过minEvictableIdleTimeMillis进行检查,否则不检查;false时,不检查
test-while-idle: true
# 检验连接是否有效的查询语句。如果数据库Driver支持ping()方法,则优先使用ping()方法进行检查,否则使用validationQuery查询进行检查。(Oracle jdbc Driver目前不支持ping方法)
validation-query: select 1 from dual
keep-alive: true
remove-abandoned: true
remove-abandoned-timeout: 80
log-abandoned: true
#打开PSCache,并且指定每个连接上PSCache的大小,Oracle等支持游标的数据库,打开此开关,会以数量级提升性能,具体查阅PSCache相关资料
pool-prepared-statements:true
max-pool-prepared-statement-per-connection-size:20
# 配置间隔多久启动一次DestroyThread,对连接池内的连接才进行一次检测,单位是毫秒。
#检测时:
#1.如果连接空闲并且超过minIdle以外的连接,如果空闲时间超过minEvictableIdleTimeMillis设置的值则直接物理关闭。
#2.在minIdle以内的不处理。
# 单位是ms
jdbc:mysql://XX.XX.XX.XX:3306/shop?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
&connectionTimeout=3000&socketTimeout=1200
3.4 分布式部署
四、多级缓存思考
4.1 多级缓存
1、 浏览器缓存
2、 CDN缓存(静态资源:js,css,视频,文件)
3、 接入层nginx/openresty缓存
4、 堆内存缓存(jvm进程级别的缓存)
5、分布式缓存(redis,memcached)
6、数据库缓存(压力非常小)
4.2 缓存架构
1、堆内存缓存
2、redis分布式缓存
3、openresty内存字典(lua)
4、lua+redis
1、jvm堆内存资源非常宝贵(classloader文件,java对象,对象管理),改如何考量?
2、内存脏数据非常的不敏感(Map: key,value)
3、内存资源分配不可控
问题1:内存资源非常宝贵,不能放入太多缓存数据,只需要放入热点数据即可,提升服务性能
问题2:定时消耗内存对象数据(定时器),数据有过期时间(定时销毁)--相当麻烦 --- GuavaCache
问题3:不可能把所有的资源都放入内存中,只放入热点数据即可
4.3 本地缓存+分布式缓存
1、先从jvm堆内存中命中(查询)缓存数据
2、如果缓存不存在,查询redis分布式缓存,如果命中,直接返回数据,放入本地缓存
3、如果分布式缓存也没有数据,查询数据库,同时把数据放入redis缓存
4.4 Openresty内存字典
问题
什么样的缓存,性能最好的?--- 离请求越近的地方,缓存数据性能越好,以为系统性能越强;
# 安装openresty:
1、wget https://openresty.org/download/openresty-1.19.3.1.tar.gz
2、tar -zxvf
3、./configure
4、make && make install # 默认被安装到/usr/local/openresty
# content_by_lua 接入lua脚本
location /lua1 {
default_type text/html;
content_by_lua 'ngx.say("hello lua!!")';
}
# content_by_lua_file 通过文件的方式引入lua脚本
location /lua2 {
default_type text/html;
content_by_lua_file lua/test.lua; # test.java ,test.py
}
# test.lua
local args = ngx.req.get_uri_args() # 获取参数对象
ngx.say("hello openresty! lua is so easy!==="..args.id) # 获取参数值,组装值:..
# 转发请求
location /lua3 {
content_by_lua_file lua/details.lua;
}
# details.lua
ngx.exec('/seckill/goods/detail/1'); # 转发请求
-- 基于内存字典实现缓存
-- 添加缓存实现
function set_to_cache(key,value,expritime)
-- 判断时间是否存在
if not expritime then
expritime = 0
end
-- 获取本地内存字典对象
local ngx_cache = ngx.shared.ngx_cache
-- 向本地内存字典添加缓存数据
local succ,err,forcibel = ngx_cache:set(key,vlaue,expritime)
return succ
end
-- 获取缓存实现
function get_from_cache(key)
-- 获取本地内存字典对象
local ngx_cache = ngx.shared.ngx_cache
-- 从本地内存字典中获取数据
local value = ngx_cache:get(key)
return value
end
-- 利用lua脚本实现一些简单业务
-- 获取请求参数对象
local params = ngx.req.get_uri_args()
-- 获取参数
local id = params.id
-- 先从内存字典获取缓存数据
local goods = get_from_cache("seckill_goods_"..id)
-- 如果内存字典中没有缓存数据
if goods == nil then
-- 从后端服务(缓存,数据库)查询数据,完毕在放入内存字典缓存即可
local res = ngx.location.capture("/seckill/goods/detail/"..id)
-- 获取查询结果
goods = res.body
-- 向本地内存字典添加缓存数据
set_to_cache("seckill_goods_"..id,goods,60)
end
-- 返回结果
ngx.say(goods)
4.5 Lua+redis
五、秒杀下单业务分析(AOP锁&分布式锁)
5.1秒杀业务实现
1、检查库存是否存在
2、扣减库存
3、更新库存
4、下单实现
1、如何在高并发情况下,保证库存不会出现超卖现象
2、如果在高并发模式下,解决下单性能问题
3、如果在高并发模式下,保证数据一致性问题
5.2 防止超卖问题
1、对共享资源(库存)加锁
2、Redis原子操作特性
3、队列 (利用队列的单线程特性)
①扣减库存:hincrement(“seckill_goods_stock_1”,-1) ; 此操作是一个原子操作 --- 多个线程也是要排队
②判断库存是否存在
说明:以上操作既解决性能问题,又解决库存超卖的问题
①队列的长度等于商品个数(pod一个队列,相当于扣减了一个库存,且队列操作是一个原子操作)
②队列中存储的数据是对应商品的ID值
③每一个商品都对应一个队列
5.3超卖问题处理
@Transactional
@Override
public HttpResult startKilled(Long killId, String userId) {
try {
//实现一个加锁的动作
lock.lock();
// do your business
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
return null;
}
/**
* @ClassName ServiceLock
* @Description
* @Author Ylc
* @Date 2022/5/28 23:12
* @Version V1.0
**/
@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {
String descripiton() default "";
}
/**
* @ClassName LockAspect
* @Description
* @Author ylc
* @Date 2022/5/28 23:13
* @Version V1.0
**/
@Component
@Scope
@Aspect
@Order(1)
public class LockAspect {
// 定义锁对象
private static Lock lock = new ReentrantLock(true);
// service 切入点
@Pointcut("@annotation(com.sugo.seckill.aop.ServiceLock)")
public void lockAspect(){
}
@Around("lockAspect()")
public Object around(ProceedingJoinPoint joinPoint){
Object obj = null;
// 开始加锁-- 方法增强
lock.lock();
try {
//执行业务
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
// 释放锁
lock.unlock();
}
return obj;
}
}
总结
原则上,构建完整一个系统,整体思路上还需考虑压力测试、分布式环境下数据一致性、接口幂等性问题,在此就不赘述。
实现压力测试(及时发现系统的问题,发现系统性能瓶颈),根据压力测试结果对系统进行优化,问题修复,压力测试验证性能是否有提升;服务端优化(tomcat服务器优化,undertow服务器优化),压力测试验证性能提升结果。
另外涉及Kubernetes原生迁移也是一项架构师领域考虑的问题。甚至全局规划人力成本这一计算很复杂的课题,会涉及时间成本、代码量成本、需求管理成本等各类成本。
随着云技术的快速迭代,完成对上述多领域新旧技术的综合架构衡量,对于非互联网企业内部IT团队技能储备已构成不小的挑战,作为云和安全服务商,新钛云服致力于提供一站式二线专家咨询支持到实施部署及代运维。
了解新钛云服
新钛云服荣膺第四届FMCG零售消费品行业CIO年会「年度数字化服务最值得信赖品牌奖」
新钛云服三周岁,公司月营收超600万元,定下百年新钛的发展目标
当IPFS遇见云服务|新钛云服与冰河分布式实验室达成战略协议
新钛云服正式获批工信部ISP/IDC(含互联网资源协作)牌照
新钛云服,打造最专业的Cloud MSP+,做企业业务和云之间的桥梁
往期技术干货
刚刚,OpenStack 第 19 个版本来了,附28项特性详细解读!
OpenStack与ZStack深度对比:架构、部署、计算存储与网络、运维监控等
点👇分享
戳👇在看
微信扫码关注该文公众号作者