网站首页高可用
需求:
高并发是我们项目开发中都会重视的问题,一个项目在经历大量并发时如果没有妥善处理,那么很可能导致服务器崩溃,其中项目首页作为访问的入口,必然会有大量并发访问冲击,是最有可能出现问题的地方,如何解决首页的高可用问题呢?
手段:
我们可以使用页面静态化+Nginx通过缓存预热,网关限流来保证首页的高可用问题,其中还可以在Nginx中做一个本地缓存(二级缓存)
页面静态化
优点:动态页面静态化技术将前端网页的动态数据静态化,访问速度快,防止sql注入,有利于网站SEO,便于搜索引擎收录
缺点:页面静态化会生成大量页面,占据磁盘的内存,其中有利于SEO的优点随着搜索引擎的发展,对于动态页面的收录将会更加容易完成。页面静态化生成的大量页面不利于维护,增加了我们的开发时间。
所以页面静态化技术要看需求使用,如果是实时性很高的网站不适合使用,数据量大但是访问量少的页面不适合使用,或者不愿意被搜索引擎爬虫收录的网站不适合使用
缓存预热
如果我们的网站没有达到像京东一样的并发量时,个人是不建议使用页面静态化技术的。一般情况下使用Nginx和redis来完成缓存预热,并且还可在Nginx做本地缓存,如此就可以应付首页的高可用问题了。
Nginx性能极高,可以处理几万并发,相比于只能处理1000并发左右的tomcat,已经是天壤之别了,所以我们采用Nginx来作为web服务器,不过它有个缺陷,nginx是使用异步非阻塞的,所以我们使用OpenResty,OpenResty封装了nginx,并且集成了LUA脚本,开发人员只需要简单的其提供了模块就可以实现相关的逻辑,不需要在nginx中自己编写
lua的脚本,再进行调用了。
以首页的广告为例:

1.1实现思路:
(1)连接mysql ,按照广告分类ID读取广告列表,转换为json字符串。
(2)连接redis,将广告列表json字符串存入redis 。
1.2定义接口:
请求 : /ad_update
参数 : position ‐‐指定广告位置
返回值 : json
1.3实现:
创建一个文件夹保存ad_load脚本,路径为/root/lua/ad_update.lua。
编写代码连接数据库查询数据并存储到redis中。
ngx.header.content_type="application/json;charset=utf8"
--引入其他模块
local cjson = require("cjson")
local mysql = require("resty.mysql")
--获取请求中的参数ID
local uri_args = ngx.req.get_uri_args()
local position = uri_args["position"]
--获取一个mysql连接
local db = mysql:new()
--设置超时时间
db:set_timeout(1000)
--修改成你自己的数据库
local props = {
host = "192.168.200.128",
port = 3306,
database = "changgou_business",
user = "root",
password = "root"
}
--连接mysql获得数据
local res = db:connect(props)
local select_sql = "select url,image from tb_ad where status ='1' and
position='"..position.."'"
res = db:query(select_sql)
db:close()
--获取一个redis连接
local redis = require("resty.redis")
local red = redis:new()
red:set_timeout(2000)
--改成你的redis
local ip ="192.168.200.128"
local port = 6379
red:connect(ip,port)
--把数据缓存redis
red:set("ad_"..position,cjson.encode(res))
red:close()
--成功提示一个true
ngx.say("{flag:true}")
修改/usr/local/openresty/nginx/conf/nginx.conf文件
http {
# 略
server{
# 在这个大括号里面添加这两个代码块就可以了,意为 当你访问 http://localhost/ad_read
的时候就执行下边这个lua文件
# 缓存预热接口
location /ad_update {
content_by_lua_file /root/lua/ad_update.lua;
}
# 二级缓存接口
location /ad_read {
content_by_lua_file /root/lua/ad_read.lua;
}
}
}
重新启动Nginx
测试: http://localhost/ad_update?position=web_index_lb
本地缓存(二级缓存)

2.1实现思路:
通过lua脚本开辟内存来保存redis的数据
2.2定义接口:
请求 : /ad_read
参数 : position ‐‐指定广告位置
返回值 : json
2.3实现:
创建一个文件夹保存ad_load脚本,路径为/root/lua/ad_read.lua。
把redis数据缓存到本地
--设置响应头类型
ngx.header.content_type="application/json;charset=utf8"
--获取请求中的参数ID
local uri_args = ngx.req.get_uri_args();
local position = uri_args["position"];
--获取本地缓存
local cache_ngx = ngx.shared.dis_cache;
--根据ID 获取本地缓存数据
local adCache = cache_ngx:get('ad_cache_'..position);
if adCache == "" or adCache == nil then
--引入redis库
local redis = require("resty.redis");
--创建redis对象
local red = redis:new()
--设置超时时间
red:set_timeout(2000)
--连接
local ok, err = red:connect("192.168.200.128", 6379)
--获取key的值
local rescontent=red:get("ad_"..position)
--输出到返回响应中
ngx.say(rescontent)
--关闭连接
red:close()
--将redis中获取到的数据存入nginx本地缓存
cache_ngx:set('ad_cache_'..position, rescontent, 10*60);
else
--nginx本地缓存中获取到数据直接输出
ngx.say(adCache)
end
修改nginx配置文件vi /usr/local/openresty/nginx/conf/nginx.conf ,http节点下添加配置:
#包含redis初始化模块
lua_shared_dict dis_cache 5m; #共享内存开启
重启Nginx,测试: http://localhost/ad_update?position=web_index_lb
Nginx限流
如果遇到大量恶意请求的话,缓存预热加二级缓存也不顶用,这时候我们可以使用Nginx限流的方式预防这个问题,nginx提供两种限流的方式: 一是控制速率 二是控制并发连接数。
其中漏桶算法,就是控制速率的一种,设置每秒通过的请求数量,大于这个数量的则请求失败,具体实现请面向浏览器编程。
Canal
canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据,借助它可以实现数据库至索引库的数据同步。
原理:
- canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
- mysql master收到dump请求,开始推送binary log给slave(也就是canal)
- canal解析binary log对象(原始为byte流)
大概就是cannal假装成mysql的儿子,骗他钱(让mysql提供数据)消费,记住,mysql需要开启binlog模式。
查看是否开启binlog模式
SHOW VARIABLES LIKE '%log_bin%'
如果 log_bin的值为OFF是未开启,为ON是已开启。
cannal-server服务的部署是结合zk实现多节点高可用部署
没有开启或者连canal服务都没有安装配置的话,请面向百度编程。
一.首页广告缓存更新
之前做过一个电商项目,网站的首页通常并发量都很高,如果从数据库中查询的话,会让产品经理把牙齿笑掉。我们通常对首页处理是先进行缓存预热,通过Nginx将数据库的数据都存往redis等nosql数据库,之后再进行多级缓存等处理。
需求:如果数据库中首页广告的数据发生了变化,此时需要再次进行缓存预热,同步redis中的数据。

代码具体实现:
一.创建数据监控服务
1.导入依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.changgou</groupId>
<artifactId>changgou_parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou_canal</artifactId>
<dependencies>
<--canal服务-->
<dependency>
<groupId>com.xpand</groupId>
<artifactId>starter-canal</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
#rabbit消息队列
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
</dependencies>
</project>
2.配置application.properties
canal.client.instances.example.host=192.168.200.128
canal.client.instances.example.port=11111
canal.client.instances.example.batchSize=1000
spring.rabbitmq.host=192.168.200.128 #mqIP地址
重点:编写监听类
package com.itheima.canal.listener;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.xpand.starter.canal.annotation.CanalEventListener;
import com.xpand.starter.canal.annotation.ListenPoint;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author ZJ
*/
@CanalEventListener //声明是canal监听类
public class BusinessListener {
@Autowired
private RabbitTemplate rabbitTemplate;//注入rabbitTemplate
/**
*
* @param eventType 当前操作数据库的类型 增删改查
* @param rowData 当前操作数据库的数据 变化前后的数据
*/
@ListenPoint(schema = "changgou_business", table = {"tb_ad"}) //设置监听的数据库和数据表
public void adUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
System.err.println("广告数据发生变化");
/**
* 思路:1.先创建一个队列ad_update_queue
* 2.将更新后的数据得到,取其中的position字段(广告位置key)数据(其中在缓存预热中存储到redis中的其实
* 是url和image两个字段,但得到position位置字段让运营服务请求Nginx访问mysql,position是缓存预热请求的参数
* nginx知道数据发生改变 再次进行缓存预热)
* 3.将position的信息通过MQ发送到运营服务
* 4.运营服务收到消息,取出position,将其作为参数再次缓存预热
* 5.进行测试
*/
//修改后数据
for(CanalEntry.Column column: rowData.getAfterColumnsList()) {
if(column.getName().equals("position")){
System.out.println("发送消息到mq ad_update_queue:"+column.getValue());
rabbitTemplate.convertAndSend("","ad_update_queue",column.getValue()); //发送消息到mq
break;
}
}
}
}
配置rabbitMQ,声明队列,使用的是简单模式,生产者-队列-消费者 一对一
package com.itheima.canal.conf;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
//定义队列名称
public static final String AD_UPDATE_QUEUE="ad_update_queue";
//声明更新首页广告队列
@Bean
public Queue queue(){
return new Queue(AD_UPDATE_QUEUE);
}
}
启动类上开启cannal服务
@SpringBootApplication
@EnableCanalClient
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class, args);
}
}
数据监控服务启动后会接收数据同步中间件canal传来的数据库的改变数据,同时将position这个字段传入消息中间件MQ中,这个字段是lua脚本暴露接口所需要的参数,保存的是广告的位置信息,MQ将该数据传给负责首页广告业务的服务中
建立首页广告运营服务
具体业务略,重点在该服务类中建立MQ消息监听类来接收MQ消息:
package com.changgou.business.listener;
import okhttp3.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 监听MQ发来的消息再发送缓存更新的请求
*/
@Component
public class CannalListener {
@RabbitListener(queues = "ad_update_queue")
public void receiveMessage(String message){
//服务的请求调用对象 类似restTemplate
System.out.println("接收消息:"+message);
OkHttpClient okHttpClient=new OkHttpClient();
//将接受到的参数并入访问Nginx中的lua脚本的url中
String url="http://192.168.200.128/ad_update?position="+message;
Request request = new Request.Builder().url(url).build();
//发起请求
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
System.out.println("请求失败");
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println("请求成功"+response.message());
}
});
}
}
代码中@RabbitListener注解用于监听对应队列消息,当消息接收成功之后,运营服务访问暴露的lua接口,发送请求至Nginx中的lua脚本开始查询mysql数据库中的变更数据,并把数据传入redis中进行缓存,完成广告的同步缓存预热。