网站首页广告的同步缓存预热--canal

网站首页高可用

需求:
       高并发是我们项目开发中都会重视的问题,一个项目在经历大量并发时如果没有妥善处理,那么很可能导致服务器崩溃,其中项目首页作为访问的入口,必然会有大量并发访问冲击,是最有可能出现问题的地方,如何解决首页的高可用问题呢?
手段:
       我们可以使用页面静态化+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可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据,借助它可以实现数据库至索引库的数据同步。

原理:

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
  2. mysql master收到dump请求,开始推送binary log给slave(也就是canal)
  3. 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中进行缓存,完成广告的同步缓存预热。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。