秒杀项目
电商秒杀系统-核心高性能解决方案
分层设计
- 接入层模型 View Object 与前端对接的模型,隐藏内部实现,仅展示的聚合模型
- 业务层模型 Domain Object 领域模型,业务核心模型,拥有生命周期贫血以及服务输出能力 (贫血模型,只有数据库对应字段,不提供其他功能,其他功能由sevice提供,比如用户模型只有username password 不会有注册等功能,充血模型则表示用户模型包括username password之外还能提供注册功能)可以处理用户生命周期,例如从注册登录到退出,包含多个数据模型,例如用户包括用户基础信息数据模型和用户密码信息数据模型
- 数据层 Data Object数据模型,同数据库映射,用以ORM方式操作数据库的能力模型
-- 用户密码会与用户信息分开存储,用户密码会放在加密数据库中,而平时只用到用户的基础信息
环境部署
jdk安装
- 下载jdk的rpm文件
- 授权最高权限 chmod 777 rpm文件
- 安装rpm文件 rpm -ivh rpm文件 (默认安装到/usr/java)
- 添加环境变量vim ~/.bash_profile
- 添加JAVA_HOME=/usr/java/jdk1.8.0_65 PATH=
JAVA_HOME/bin - 保存并退出:wq
- 刷新配置文件source ~/.bash_profile
- 添加JAVA_HOME=/usr/java/jdk1.8.0_65 PATH=
mysql安装
- 安装mysql相关依赖yum install mysql*
- rpm方式不需要镜像就能更新,但需要解决依赖
- yum方式不需要解决依赖
- yum install mariadb-server
- 启动mysql systemctl start mariadb.service
- 初始化root账户密码mysqladmin -u root password root
jar上传
- 打包成springboot应用
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
- maven clean package
- 上传将文件传送到服务器 scp *.sql root@ip://tmp/ -> -r 复制文件夹
- chmod 777 jar包
- deploy.sh 文件 nohup java -Xms400m -Xmx400m -XX:NewSize=200m -XX:MaxNewSize=200m -jar miaosha.jar --spring.config.addition-location=/var/www/miaosha/application.properties
- chmod 777 deploy.sh
安装nginx
- 下载openresty-1.13.6.2.tar.gz
- chomd -R 777 openresty-1.13.6.2.tar.gz
- tar -xvzf openresty-1.13.6.2.tar.gz
- cd openresty-1.13.6.2
- yum install pcre-devel openssl-devel gcc curl
- 编译 ./configure
- make 编译
- make install 安装
- cd /usr/local/openresty
- bin
- luajit
- nginx /sbin/nginx(这个文件替换掉就能替换了nginx)
- 启动nginx nginx/sbin/nginx -c nginx/conf/nginx.conf
- 修改配置后直接sbin/nginx -s reload 无缝重启 不影响客户连接
分布式
单台应用
- pstree -p 端口号 | wc -l 查看java线程数
- top -H查看性能数量
- %Cpu 1、us 用户态下CPU的耗时 2、sy 内核空间对CPU的耗时(socket send read) 两个加起来不能超100%
- load average 最近 1 分钟 5 分钟 15分钟的 cpu load数量,越低越好 控制在CPU数内 高表示CPU很忙(死循环us高,但load average很低)
- sever端的并发线程数上不去(spring-configuration-metadata.json文件)
server.tomcat.accept-count:等待队列长度(超过就拒绝),默认100->1000
server.tomcat.max-connections:最大可被连接数,默认10000
server.tomcat.max-threads:最大工作线程数,默认200->800 4核8G
server.tomcat.min-space-threads:最小工作线程数,默认10->100
默认配置下,连接超过10000后出现拒绝链接情况
默认配置下,触发的请求超过200+100后拒绝处理
keep-alive减少连接消耗
keepAliveTimeOut:多少毫秒后不响应的断开keepalive
maxKeepAliveRequests:多少次请求后keepalive断开失效
- 定制化内嵌tomcat配置
//当spring容器内没有TomcatEmbeddedServletContainerFactory这个bean时,会把此bean加载进spring容器中
@Component
public class WebServerConfiguration implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
((TomcatServletWebServerFactory)factory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
@Override
public void customize(Connector connector) {
Http11NioProtocol http11NioProtocol = (Http11NioProtocol) connector.getProtocolHandler();
//定制化keepalivetimeout 设置30秒内没有请求则服务器自动断开keepalive连接
http11NioProtocol.setKeepAliveTimeout(30000);
//当客户端发送超过10000个请求则自动断开keepalive连接
http11NioProtocol.setMaxKeepAliveRequests(1000);
}
});
}
}
单机容量问题(响应时间变长 TPS上不去),
- 线程数量:4核CPU 8G内存单进程调度线程数800-1000以上后即花费巨大时间在CPU调度上
- 等待队列长度:队列做缓冲池用,但也不能无限长,消耗内存,出队入队也耗CPU(1000-2000)
- 表象:单机CPU使用率增高,memory占用增加,网络带宽使用增加
- cpu us :用户空间的cpu使用情况(用户层代码)
- cpu sy:内核空间的cpu使用情况(系统调用)
- load average:1,5,15分钟load平均值,跟着核数系数,0代表正常1代表打满1+表示等待阻塞
- memory fee空闲内存,used使用内存
MySql QPS容量问题
- 主键查询:千万级别数据 = 1-10毫秒
- 唯一索引查询:千万级别数据 = 10-100毫秒
- 非唯一索引查询:千万级别数据 = 100-1000毫秒 ->分库分表,扩容热点数据
- 无索引:百万条数据 1000毫秒+ ->分库分表,扩容热点数据
Mysql TPS容量问题
- 非插入更新删除操作:同查询 ->where条件
- 插入操作:1w-10w tps(依赖配置优化)
nginx反向代理(水平扩展)
- 水平扩展->使用nginx反向代理(1个nginx,2个jar,1个mysql共4个虚拟机)
- nginx作为web服务器(静态)
- nginx作为动静分离服务器
- nginx作为反向代理服务器
- nginx生产环境使用nas(无限容量)代替本地磁盘
- OpenResty框架->nginx开发和调优OpenResty是一款基于 NGINX 和 LuaJIT 的 Web 平台。redis lua nginx配置都有
- nginx静态资源访问配置(访问/resources/都是访问静态资源)
- location节点path:指定url映射key
- location节点内容:root指定location path后对应的根路径,index指定默认的访问页 alias 指替换调路径
worker_processes 1工作进程
events{
worker_connections 1024;可以接受的工作连接
}
mine.types->content-type设置
location /resources/ { ->/resource/xxxx 替换为 /usr/local/openresty/nginx/html/resources/
alias /usr/local/openresty/nginx/html/resources/;
index index.html index.html;
}
2.反向代理配置(默认与后端没有长连接keep alive)数据库连接池和h5与nginx有keepalive,长连接
2.1设置upstream server
server{
upstream back_server {
server ip:port weight=1;
server ip:port weight=1;
keepalive 30;//设置nginx与后端长连接30s,30内没操作断开 与 http1.1配合使用
}
}
2.2设置动态请求location为proxy pass 路径
location /{
proxy_pass http://back_server;
proxy_set_header Host $http_host:$proxy_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header : X-Forwared-For $proxy_add_x_forwarded_for;//代表nginx转发请求
proxy_http_version 1.1;//使用keepalive
proxy_set_header Connection "";//使用keepalive
}
2.3开启tomcat access log验证
在application.properties配置
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.directory=/var/www/miaosha/tomcat //先创建
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D
%h->远端ip
%l-> -
%u->远端用户
%t->耗时
%r->请求信息方法和url
%s->返回状态码
%b->byte send响应大小
%D->处理请求时长
设置长连接优化 proxy_http_version 1.1;//使用keepalive proxy_ser_header Connection "";//使用keepalive
keepalive 30;//设置nginx与后端长连接30s,30内没操作断开 与 http1.1配合使用
nginx高性能原因
epoll多路复用机制完成非阻塞IO操作
- bio模型。阻塞式进程模型,write过程阻塞,等到网络完成才能继续执行、
- linux select模型 变更触发轮询查找, 有1024数量上限 首先阻塞自己,监听100个客户连接是否有变化,若有变化则唤醒自己,循环遍历100连接,找到发生变化的一个或者多个执行read操作。遍历效率低,连接数量有上限
- epoll模型,变更触发回调直接读取,理论上无上限,不会断开客户连接,监听100个客户端连接是否有变化,设置回调函数,若有变化则唤醒自己,并执行回调函数
java nio-》自动选择select或者epoll模型。linux内核2.6以上才有epoll模型
master worker 进程模型平滑加载和重启,不会断开与客户端连接
master worker父子进程->master可以管理worker进程内存空间(两者共享内存,根据worker_processes配置worker进程数),worker进程可以竞争处理客户端连接accept请求,用内存锁保证多个worker进程能有序负责某个连接,一旦accept之后,就由当时处理的worker进程一直处理下去。reload配置文件只会切换worker进程,master进程不会修改,客户端也不会断开socket连接,是因为重启的时候将所有worker进程句柄交给master进程,重启时,master进程又交回给新的worker进程去处理,每个worker进程都是单线程的,没有阻塞的话,单线程比多线程快,只做了内核空间转向外部空间的拷贝。并没有阻塞什么,所以高性能协程机制 非阻塞编程 单进程单线程
依附于线程的内存模型,切换开销小(内存切换)而线程是CPU切换开销
遇到阻塞及归还执行权,代码同步,可以处理大量的异步回调函数(大量异步函数嵌套,本身不好做顺序控制),协程可以作顺序控制,写代码可以直接return
无需加锁,串行执行
分布式会话管理
- 基于cookie传输sessionid(jsessionid):tomcat实现session(转移到redis)(移动端不行,禁cookie)
@Autowired
private HttpServletRequest httpServletRequest;
httpServletRequest.getSession.setAttribute("xx",xx);
- 基于token(令牌)实现sessionid:java实现session(uuid)(转移到redis)
请求带上?token =
后端 String token2 = this.httpServletRequest.getParameterMap().get("token")[0];//拿到
@Autowired
private RedisTemplate redisTemplate;
String uuidToken = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(uuidToken,userModel);
redisTemplate.expire(token,1, TimeUnit.HOURS);
redisTemplate.opsForValue().get(uuidToken,userModel);
下载redis
chmod -R 777 redis-5.0.4.tag.gz
tar -zxvf redis-5.0.4.tag.gz
cd redis-5.0.4
./configure
make
make install
cd src
./redis-server
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=xxx
#默认16个数据库
spring.redis.database=0
#连接池
spring.redis.jedis.pool.max-active=50
spring.redis.jedis.pool.max-idle=20
@Component
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisConfig {
//将java序列化转成json序列化
@Bean
public RedisTemplate restTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置key序列化方式->不设置则表示一定要实现序列化接口
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
//解决value的序列化方式->不设置则表示一定要实现序列化接口
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(DateTime.class, new JodaDateTimeJsonDeserializer());
simpleModule.addSerializer(DateTime.class, new JodaDateTimeJsonSerializer());
//各个属性都能有对应的解析说明,加了列的类型说明
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.registerModule(simpleModule);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
public class JodaDateTimeJsonDeserializer extends JsonDeserializer<DateTime> {
@Override
public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String dateString = jsonParser.readValueAs(String.class);
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:ss");
return DateTime.parse(dateString,formatter);
}
}
public class JodaDateTimeJsonSerializer extends JsonSerializer<DateTime> {
@Override
public void serialize(DateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(dateTime.toString("yyyy-MM-dd HH:mm:ss"));
}
}
查询多级缓存(丢失)
- 缓存设计
- 用内存做缓存
- 将缓存推到离用户最近的地方,性能越高,但是更新越难
- 脏缓存清理->数据更新了缓存没更新的情况(牺牲内存,并且有脏读问题)
- nginx proxy cache缓存
- nginx lua缓存
redis缓存
- redis缓存(网络,集中式负载均衡瓶颈)->一般把model放入到redis中缓存起来,并且记得设置超时时间,记得实现序列化接口(默认)
redis nosql数据库可以持久化,集中式缓存,可以丢失
单机,容量,故障瓶颈,所有业务炸掉
sentinal哨兵模式 redis1对应一个slave redis2,则redis1会将数据同步到redis2,redis1挂了使用redis2,两者主从关系。redis sentinel与多个redis建立长连接,通过心跳机制,而后台访问redis sentinel决定redis主从问题,最后后台直接访问该redis
容量问题明显,master不好扩容,难以复制数据
cluster集群模式
redis都在互连,使用算法保证高可用,后端只要访问其中一个redis就行
//配合RedisConfig
redisTemplate.opsForValue().set(uuidToken,userModel);
redisTemplate.expire(token,1, TimeUnit.HOURS);
redisTemplate.opsForValue().get(uuidToken,userModel);
本地缓存
- 存放热点数据,较少redis访问,减少io,在每秒上万情况下
- 脏读非常不敏感,分散,数据难同步(可以用mq解决,但没必要)
- 内存可控
- 生命周期短,比redis短
- 内存jvm本地缓存
- 方案使用Guava cache有淘汰机制,能支持并发读写(线程安全)
- 可控制的大小和超时时间
- 可配置的lru策略(最近最少访问优先淘汰)
- 线程安全
先去取本地缓存->redis->mysql
@Service
public class CacheServiceImpl implements CacheService {
private Cache<String,Object> commonCache = null;
@PostConstruct
public void init(){
this.commonCache = CacheBuilder.newBuilder()
//设置缓存容器初始容量为10
.initialCapacity(10)
//设置缓存中最大可以存储100个KEY,超过100个之后会按照LRU的策略移除缓存项
.maximumSize(100)
//设置写缓存后多少秒过期
.expireAfterWrite(30, TimeUnit.SECONDS).build();
}
@Override
public void setCommonCache(String key, Object value) {
commonCache.put(key,value);
}
@Override
public Object getCommonCache(String key) {
return commonCache.getIfPresent(key);
}
}
nginx proxy 缓存(少用,因为还是访问文件)
- nginx proxy cache缓存(不好用,因为读文件,反而比其他内存缓存慢)
- 必须是nginx反向代理前置才能生效
- 依靠文件系统存索引级文件(把请求当做文件,去本地找是否有对应文件,有则缓存生效)
- 依靠内存缓存文件地址
在nginx.conf中声明
http{
#声明一个cache缓存节点内容,生成二级目录,在url生成hash值时取最后一位当第一级目录,倒数第二位当二级目录 inactive=7d保存7天
proxy_cache_path /usr/local/openresty/nginx/tmp_cache levels=1:2 keys_zone=tmp_cache:100m inactive=7d
max_size=10g;
location /{
proxy_cache tmp_cache;
proxy_cache_key $uri;
# 返回状态码200 206 304 302
proxy_cache_valid 200 206 304 302 7d;
}
}
nginx lua缓存(放nginx内存,推荐)
nginx 内存缓存(最好热点数据)
lua协程机制,线程空间栈的执行单元,串行执行,用户态模拟出来的内存空间,以同步方式编写代码
nginx协程机制
nginx lua插载点
OpenResty
协程机制
依附于线程的内存模型,切换开销小
遇阻塞即归还执行权,代码同步
无需加锁
nginx协程
nginx的每一个worker进程都是在epoll或kqueue这种事件模型之上,封装成协程
每一个请求都有一个协程进行处理
即使ngx_lua须运行Lua,相对C有一定开销,但依旧能保证高并发能力
nginx每个worker进程都创建一个lua虚拟机
worker进程内所有协程共享同一个lua虚拟机
每个外部请求由一个lua协程处理,协程间数据隔离
lua代码 调用io等异步接口时,协程被挂起,上下文数据自动保存,不阻塞worker进程,io异步操作完成后还原协程上下文,代码继续执行
nginx处理阶段
NGX_HTTP_POST_READ_PHASE=0;//读取请求头
NGX_HTTP_SERVER_REWRITE_PHASE;//执行rewrite ->rewrite_handler定制开发
NGX_HTTP_FIND_CONFIG_PHASE;//根据uri替换location
NGX_HTTP_REWRITE_PHASE;//根据替换结果继续执行rewrite-> reweite_handler定制开发
NGX_HTTP_POST_REWRITE_PHASE;//执行rewrite后处理
NGX_HTTP_PREACCESS_PHASE;//认证预处理,请求限制,连接限制 -> limit_conn_hander_limit_req_handler定制开发
NGX_HTTP_ACCESS_PHASE;//认证处理->auth_basic_handler_access_handler定制开发
NGX_HTTP_POST_ACCESS_PHASE;//认证后处理,认证不通过,丢包
NGX_HTTP_TRY_FILES_PHASE;//尝试try标签
NGX_HTTP_CONTENT_PHASE;//内容处理->static_handler 定制开发
NGX_HTTP_LOG_PHASE;//日志处理->log_handler
nginx lua插载点
init_by_lua:系统启动时调用
init_worker_by_lua:worker进程启动时调用
set_by_lua:NGINX变量用复杂lua return
rewrite_by_lua:重写url规则
access_by_lua:权限认证阶段
content_by_lua:内容输出节点
- 使用lua
init.lua文件
ngx.lua(ngx.ERR,"init lua success");
content.lua文件
ngx.say("hello");
ngx.exec("url");//nginx代理请求
在http模块下
http{
init_by_lua_file /usr/local/init.lua;
location /xxx/ {
default_type "text/html";
content_by_lua_file /usr/local/content.lua;
}
}
- 开发OpenResty与nginx的lua(放入nginx的内存里面)
- OpenResty由Nginx核心加很多第三方模块组成,默认集成了Lua开发环境,使得Nginx可以作为一个web server使用
- 借助于nginx的时间驱动模型和非阻塞IO,可以实现高性能的web应用程序
- openResty提供了大量组件如mysql,redis,memcached等等,使在nginx上开发web应用方便简单
- shared dic共享内存字典,所有worker进程可见,lru淘汰
在nginx 文件中
http{
lua_shared_dict my_cache 128m
location /xxx/ {
default_type "application/json";
content_by_lua_file /usr/local/sharedic.lua;
}
}
sharedic.lua文件
function get_from_cache(key)
local cache_ngx = ngx.shared.my_cache
local value = cache_ngx:get(key)
return value
end
function set_to_cache(key,value,exptime)
if not exptime then
exptime = 0
end
local cache_ngx = ngx.shared.my_cache
local succ,err,forcible = cache_ngx:set(key,value,exptime)
return succ
end
local args = ngx.req.get_uri.args()
local id = agrs["id"]
local item_model = get_from_cache("item"..id)
if item_model == nill then
local resp = ngx.location.capture("/item/get?id="..id)
item_model = resp.body
set_to_cache("item_"..id,item_model,1*60)
end
ngx.say(item_model)
openresty redis支持(减少访问jar消耗,减少数据更新问题)推荐
在nginx 文件中
http{
lua_shared_dict my_cache 128m
location /xxx/ {
default_type "application/json";
content_by_lua_file /usr/local/redis.lua;
}
}
redis.lua
local args = ngx.req.get_uri.args()
local id = agrs["id"]
local cache = redis:new()
local ok,err = cache:connect("127.0.0.1",6379)
local item_model = cache:get(""item_"..id)
if item_model == ngx.null or item_model == nil then
local resp = ngx.location.capture("/item/get?id="..id);
item_model =resp.body
end
ngx.say(item_model)
动态缓存cdn(访问静态资源html)
静态请求CDN(使用阿里云CDN加速和配置CNAME)
DNS用CNAME解析到源站,域名解析分成A解析和CNAME解析等A解析就是域名->ip,而CNAME就是将请求发送到CNAME指定的服务器,然后由CNAME的服务器处理,此时CNAME的服务器会指向一个最近的服务器ip,该服务器会判断是否有缓存,没有则请求源站(回源)回源缓冲设置
cache control响应头
private 客户端可以缓存(默认)
public:客户端和代理服务器都可以缓存
max-age=xxx:缓存的内容将在xxx秒后失效
no-cache:强制向服务端再验证一次
- 有效性判断:ETag资源唯一标识,
- Etag一致则 if-None-Match 客户端发送的匹配Etag标识符->一致则304
- Etag不致则 ->Last-modified: 资源最后被修改时间 if-Modified-Since:客户端发送的匹配资源最后修改时间的标识符 ->一致则304
no-store:不缓存请求的任何返回内容
浏览器的三种刷新方式
回车或a链接:cache-control对应的max-age是否仍然有效,有效则直接from cache,若cache-control中为no-cache,则进入缓存协商逻辑
F5刷新或command+R刷新:去掉cache-control中的max-age或直接设置max-age为0,然后进入缓存协商逻辑
ctrl+F5或commond+shift+R刷新:去掉cache-control和协商头,强制刷新
协商机制,比较Last-modified和Etag到服务端,若服务端判断没变化则304不返回数据否则200返回数据
CDN自定义缓存策略
可自定义目录过期时间
可自定义后缀名过期时间
可自定义对应权重
可通过界面或api强制刷新cdn对应目录刷新(非保成功,有通信限制)
静态资源cdn深入讲解--CDN自定义缓存策略04:16
静态资源部署策略
html必定no-cache或者max-age很短,便于更新。html文件或者设置较长的max age。依靠动态的获取版本号请求(对比版本号)发送到后端,异步下载最新的版本号的html后展示渲染在前端
css,js,img等元素使用带版本号部署,a.js?v=1.0,不便利,维护困难
css,js,img等元素使用带摘要部署,例如a.js?v=45edw存在先部署html还是先部署资源的覆盖问题,因为所有html与资源都要更新
(推荐)css,js,img等元素使用摘要做文件名,例如45edw.js,新老版本并存且可回滚,资源部署后再部署html->css,js,img对应静态资源保持生命周期内不会变,max-age可设置的很长,无视失效更新周期
动态请求也可以静态化成json资源推送到cdn上(获取商品)
前端先显示旧版本,因为cdn推送不一定成功,所以可以再依靠异步请求获取后端节点比对版本号,不一致则可以对对应资源状态做紧急下架处理(覆盖)
可通过跑批紧急推送cdn内容以使其下架等操作
强推失效
全页面静态化
html,css,js静态资源cdn化+js ajax 动态请求cdn化=全页面静态化
在服务端完成html,css,甚至js的load渲染成html文件后直接以静态资源的方式部署到cdn上(已渲染好的页面)
使用phantomjs(本质爬虫)
无头浏览器,可以借助器模拟webkit js的执行
修改需要全页面静态化的实现,采用initView和hasInit方式防止多次初始化(写程序保证只加载一次)
编写对应轮询生成内容方式
将全静态话页面生成后推送到cdn
xxx.js
var page = require("webpage").create();
var fs = require("fs");
page.open("http://xxxx",function(status){
console.log("status = " + status);
setTimeout(function(){
//page.evaluate(function(){
//调用拿到的请求的html里面的函数
//});
fs.write("xxxx.html",page.content,"w");
phantom.exit();
},1000);
})
bin/phantomjs js/genitem.js
动态请求缓存
页面静态化
交易泄压
缓存库存
交易性能瓶颈
交易验证完全依赖数据库(判断商品和用户,活动信息)
数据库行锁;某个id(扣减库存)
后置处理逻辑(保存订单,增加销量)
交易验证优化
用户风控策略优化:策略缓存模型优化(放redis并设置超时时间)
活动校验策略优化(放redis并设置超时时间):引入活动发布流程,模型缓存化,紧急下线能力(异常商品去掉redis缓存即可)
库存行锁优化
update的时候如果没有指定索引的话会锁表,有索引的话有行锁,行锁保证某条数据的更新操作串行化
扣减缓存异步化->1.活动发布同步库存进缓存->2.下单交易减库存缓存
异步同步数据库->3.异步事务型消息扣减数据库内库存(异步消息队列rocketmq)高性能,高并发,分布式消息中间件
应用,分布式事务,异步解耦(producer,name server consumer(注册中心),consumer group(consumer),broker(queue))->引入库存操作流水(库存数量,商品id,操作流水号,订单状态,操作前insert初始状态,操作完update为已扣减库存状态,操作中select查看操作状态,若为已扣减,则提交事务,否则回滚事务)->库存数据库最终一致性保证
redis不能用时如何处理?扣减流水错误如何处理?
库存设计原则:
宁可少卖,不能超卖
redis可以比实际数据库中少,订单长时间没处理时,需要超时释放库存,把库存加回去
分布式事务
CAP理论,最终一致性BASE
安装rocketmq
下载rocketmq
chomd -R 777
yum install unzip
unzip rocketmq-all-4.4.0-release.zip
cd rocketmq-all-4.4.0-release
nohup sh bin/mqnamesrv(9876端口)
nohup sh bin/mqbroker -n localhost:9876
mq.nameserver=xxxx:port
mq.topicname=xx
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
//异步修改库存,要保证数据库插入订单和redis扣减库存成功后才异步把数据库扣减
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
super.afterCommit();
}
});
确保最近的事务提交完成后会触发
@Component
public class MqProducer {
@Autowired
private DefaultMQProducer producer;
@Autowired
private TransactionMQProducer transactionMQProducer;
@Value("${mq.nameserver.addr}")
private String nameAddr;
@Value("${mq.topicname}")
private String topicName;
@PostConstruct
public void init() throws MQClientException {
producer = new DefaultMQProducer("producer_group");
producer.setNamesrvAddr(nameAddr);
producer.start();
transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
transactionMQProducer.setNamesrvAddr(nameAddr);
transactionMQProducer.start();
transactionMQProducer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
//真正要做的事 创建订单
Integer itemId = (Integer) ((Map)arg).get("itemId");
Integer promoId = (Integer) ((Map)arg).get("promoId");
Integer userId = (Integer) ((Map)arg).get("userId");
Integer amount = (Integer) ((Map)arg).get("amount");
String stockLogId = (String) ((Map)arg).get("stockLogId");
try {
orderService.createOrder(userId,itemId,promoId,amount,stockLogId);
} catch (BusinessException e) {
e.printStackTrace();
//设置对应的stockLog为回滚状态
StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
stockLogDO.setStatus(3);
stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
@Override
//事务执行时间太长的时候rocketmq要回调这个方法,这里应该检查redis库存
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
//查询操作流水中操作的状态,用数据库并传入流水号
//如果订单状态为已扣减,则成功,否则unknown
return null;
}
});
}
//事务性扣减库存
//事务型同步库存扣减消息
public boolean transactionAsyncReduceStock(Integer userId,Integer itemId,Integer promoId,Integer amount,String stockLogId){
Map<String,Object> bodyMap = new HashMap<>();
bodyMap.put("itemId",itemId);
bodyMap.put("amount",amount);
bodyMap.put("stockLogId",stockLogId);
Map<String,Object> argsMap = new HashMap<>();
argsMap.put("itemId",itemId);
argsMap.put("amount",amount);
argsMap.put("userId",userId);
argsMap.put("promoId",promoId);
argsMap.put("stockLogId",stockLogId);
Message message = new Message(topicName,"increase",
JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
TransactionSendResult sendResult = null;
try {
sendResult = transactionMQProducer.sendMessageInTransaction(message,argsMap);
} catch (MQClientException e) {
e.printStackTrace();
return false;
}
if(sendResult.getLocalTransactionState() == LocalTransactionState.ROLLBACK_MESSAGE){
return false;
}else if(sendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE){
return true;
}else{
return false;
}
}
//无事务发送
public boolean send(String itemId,Integer amount) {
Map<String,Object> bodyMap = new HashMap<>();
bodyMap.put("itemId",itemId);
bodyMap.put("amount",amount);
Message message = new Message(topicName,"increse",
JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
try {
producer.send(message);
} catch (MQClientException e) {
e.printStackTrace();
return false;
} catch (RemotingException e) {
e.printStackTrace();
return false;
} catch (MQBrokerException e) {
e.printStackTrace();
return false;
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
return true;
}
}
@Component
public class MqConsumer {
@Autowired
private DefaultMQPushConsumer consumer;
@Value("${mq.nameserver.addr}")
private String nameAddr;
@Value("${mq.topicname}")
private String topicName;
@PostConstruct
public void init() throws MQClientException {
consumer = new DefaultMQPushConsumer("stock_consumer_group");
consumer.setNamesrvAddr(nameAddr);
consumer.subscribe(topicName,"*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
Message msg = list.get(0);
String body = new String(msg.getBody());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
}
}
交易异步化
异步化事务
操作流水
数据类型
主业务数据master data
操作性数据log data
库存售罄
库存售罄标识,放到redis
出售前判断是否售罄,售罄后不去操作后续流程
售罄后通知各系统售罄
回补上新
后置流程
销量逻辑异步化
交易单逻辑异步化(生成交易单sequence后直接异步返回,前端轮询异步单状态)
流量错峰
流量削峰技术
秒杀下单接口会被脚本不停的刷
秒杀验证逻辑和秒杀下单接口强关联,代码冗余度高->引入秒杀令牌
秒杀验证逻辑复杂,对交易系统产生无关联负载->引入秒杀令牌
秒杀令牌
秒杀令牌原理(少流量)
秒杀接口需要依靠令牌才能进入(uid+商品id+秒杀id)
秒杀的令牌由秒杀模块生成
秒杀活动模块对秒杀令牌生成全权处理(将验证用户,商品,活动迁移到令牌生成,没有令牌不能下单,放redis,前端需要单独获取秒杀令牌,后端单独写秒杀令牌),逻辑收口(没有令牌不往下走)
秒杀下单前需要获得秒杀令牌,没有令牌则不能下单
秒杀大闸
秒杀令牌只要活动一开始就无限制生成,影响系统性能
解决方案:依靠秒杀令牌的授权原理定制化发牌逻辑,做到大闸功能,
根据秒杀商品初始库存颁发对应数量的令牌,控制大闸流量,在redis保存库存数乘以系数个数量,然后放到redis中,每次生成秒杀令牌减掉1然后为0则不再发牌
用户风控策略前置到秒杀令牌发放中
库存售罄判断前置到秒杀令牌发放中
队列泄洪
浪涌流量涌入后系统无法应对(超大流量)
多库存,多商品等令牌限制能力弱
解决方案->队列泄洪原理
排队有时候比并发更高效(redis单线程模型 innodb mutex key等)
依靠排队去限制并发流量
依靠排队和下游拥塞窗口程度调整队列释放流量大小->支付宝银行网关队列举例,支付宝做拥塞窗口处理,保证下游银行的tps
实现 本地:将队列维护在本地内存中(生产环境,推荐)
分布式:将队列设置到外部redis内,性能太差,负载大
企业一般是集中式异常,超时后转本地
private ExecutorService executorService;
@PostConstruct
public void init(){
executorService = Executors.newFixedThreadPool(20);
}
Future<Object> submit = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
return null;
}
});
try {
submit.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return;
防刷限流
验证码
包装秒杀令牌前置,需要验证码来错峰
数学公式验证码生成器,生成验证码返回给前端,然后放入redis在生成秒杀令牌时校验一下,没有校验过的不发放秒杀令牌
限流器
系统宁愿只让少数人能用,不能让所有人不能用
方案
1.限制并发 tps 每秒处理请求数量(不用)
2.令牌桶算法 令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。(常用)
3.漏桶算法,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
限流力度
接口维度(限制controller)
总维度(计算机资源限制)
限流范围
集群限流:依赖redis或其他中间件技术做统一计数器,往往会产生性能瓶颈
单机限流:负载均衡的前提下单机平均限流效果更好
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
private RateLimiter rateLimiter;
@PostConstruct
public void init(){
rateLimiter = RateLimiter.create(300);
}
if(!rateLimiter.tryAcquire()){
throw new RuntimeException("xxx");
}
防黄牛
排队,限流,令牌均只能控制总流量,无法控制黄牛流量
传统防刷
限制一个会话(session_id,token)同一秒钟/分钟接口调用多少次->多会话接口绕开无效
限制一个ip同一秒钟/分钟 接口调用多少次,数量不好控制->容易误伤
使用设备指纹防刷
采集终端设备各项参数,启动应用时生成唯一设备指纹
根据对应设备指纹的参数猜测出模拟器等可疑设备概率
凭证系统
根据设备指纹下发凭证
关键业务链路上带上凭证并由业务系统到凭证服务器上验证
凭证服务器根据对应凭证所等价的设备指纹参数并根据实时行为风控系统判定对应凭证的可疑分数
若分数低于某个数值则业务系统返回固定错误码,拉起前端验证码验身,验身成功后加入凭证服务器对应分数
H5最难防
性能测试
jemeter压测
- 线程组 高并发压测 ->发送http请求 ->查看结果树 ->聚合报告 tps qps :
- 新建测试计划
- 添加线程组->线程数->Ramp-up时间(多少秒内启动所有线程)->循环次数(每个线程调用接口几次)
- 添加http请求->名称->协议->服务器名称ip->请求方法->路径,同时选择keep-alive,因为要测的是返回时间而不是连接断开或者连接的时间
- 高级->客户端实现一定要选java
- 添加察看结果树
- 添加聚合报表
- Throughput (tps)每秒能接受多少流量
- Average平均响应时间
- Median中位线响应时间
- 90%Line 90%响应时间小于多少毫秒
- Min最小返回时间
- 添加http请求->名称->协议->服务器名称ip->请求方法->路径,同时选择keep-alive,因为要测的是返回时间而不是连接断开或者连接的时间
- 添加线程组->线程数->Ramp-up时间(多少秒内启动所有线程)->循环次数(每个线程调用接口几次)
压测优化
分布式会话管理
分布式会话持久性管理->放redis,一定要redis高可用(redis cluster)
会话有效期时间,tomcat默认30分钟,不与服务器发生交互的呆滞时间
会话续命管理,触发操作延长生命周期,延长到30min,不是加30min
安全性问题,用安全传输的https,但也会被浏览器捕获,最安全使用自定义协议并使用app提高安全性
强登录态与弱登录态
强登录态:>需要登录才能操作
无需登录
弱登录态: >千人千面的智能推荐,电商,续命能力弱
弱登录续命:>1.请求续命,2.页面使用定时器请求续命(keepalive续命)
sso单点登录
只要域名不一样时,cookie就不一样
- 同域名:>cookie一样,wwww.xxx.com/a/ wwww.xxx.com/b/
- 根域名相同,子域名不同 wap.a.com www.a.com
- httpOnly = false/true(除了浏览器自身,js无法访问cookieid)
- domain =/ 只要访问同一个一级域名时cookie域名就一样
- 域名都不相同 a.com b.com 通过sso服务配合redirect,颁发cookie,然后返回需要请求的服务,服务缓存起来,后再去sso校验,成功后就继续服务,cookie颁发后一定要有有效期,或者强制更改手段,手动删除
mysql应用性能优化拓展:>缓存,异步,批处理
写操作:批量insert 批量update 批量写,sql编译N次和1次的时间与空间复杂度,网络消耗的时间复杂度 磁盘寻址的复杂度
读操作:索引
mysql单机配置性能优化拓展 执行sql前先写undo/redo日志顺序写,写数据则是随机读写
max_connection=1000默认100
innodb_file_per_table=1,每个table一个文件
innodb_buffer_pool_size=1G 写数据缓冲区大小,越大越容易命中buffer缓存,内存60%-80%
innodb_log_file_size=256M redo/undo日志大小
innodb_log_buffer_size=16M 日志满了切换的时候添加buffer缓冲
innodb_flush_log_at_trx_commit=2需要放在【mysqlId_safe】节点下,默认1,事务提交立马刷盘
innodb_data_file_path=ibdata1:1G;ibdata2:1G;ibdata3:1G;autoextend,数据量大时分区
秒杀系统扣减库存
-- 库存未预热
if (redis.call('exists', KEYS[2]) == 1) then
return -9;
end;
-- 秒杀商品库存存在
if (redis.call('exists', KEYS[1]) == 1) then
local stock = tonumber(redis.call('get', KEYS[1]));
local num = tonumber(ARGV[1]);
-- 剩余库存少于请求数量
if (stock < num) then
return -3
end;
-- 扣减库存
if (stock >= num) then
redis.call('incrby', KEYS[1], 0 - num);
-- 扣减成功
return 1
end;
return -2;
end;
-- 秒杀商品库存不存在
return -1;