5. 畅购商城之Lua、Canal实现广告缓存

第5章 Lua、Canal实现广告缓存

1. 首页分析

首页门户系统需要展示各种各样的广告数据。以京东为例:
在这里插入图片描述
页面中的广告一般来说变变更频率较低,对于这种数据该如何进行处理?

(1) 第一种方式
在这里插入图片描述
如图所示,首页访问广告服务,而广告服务从数据库中查询数据,而后返回给首页进行展示。这种方式最为简单。但是首页的访问量一般非常高,不适合直接通过MySQL数据库直接访问的方式来获取展示。
(2) 第二种方式
在这里插入图片描述
1.首先访问Nginx ,采用缓存的方式,先从Nginx本地缓存中获取,获取到直接响应

2.如果没有获取到则访问Redis,从Redis中获取数据,如果有数据则返回,并缓存到Nginx中

3.如果没有获取则访问MySQL,从MySQL中获取数据,再将数据存储到Redis中,返回。

2. Lua介绍

2.1 Lua是什么

Lua [1] 是一种轻量小巧的脚本语言。其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能的弱语言,不需要编译可以直接运行。Lua是由标准C编写而成,所有Lua脚本可以容易被C/C++代码调用,其他所有操作系统和平台也可以进行编译、运行

Lua是一种功能强大、高效、轻量级、可嵌入的脚本语言,支持过程编程、面向对象编程、函数编程、数据驱动编程和数据描述;

2.2 优势

  • 轻量级
    轻量级Lua语言的官方版本只包含一个简洁的核心和最基本的库,Lua体积小、启动速度快,5.0.2版本Lua内核只有120kb,适合嵌入别的程序;

  • 可扩展
    Lua由标准C编写,所以C/C++的功能都可以使用,而且还可以扩展Java、C#、Smalltalk、Fortran、Ada、Perl和Ruby;

  • 可移植
    Lua使用C编写,所以适用所有操作系统和平台(Windiows/Unix、IOS、Android、BREW、Symbian、WindowPhone、Rabbit等等);

  • 完全开源免费
    Lua是免费的开源软件,可以用于任何目的,包括商业目的完全免费。

2.3 应用场景

  • 游戏开发
  • 独立应用脚本
  • Web 应用脚本
  • 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
  • 安全系统,如入侵检测系统
  • Redis中嵌套调用实现类似事务的功能
  • web容器中应用处理一些过滤 缓存等等的逻辑,例如Nginx。

2.4 阿里云安装Lua

安装步骤,在服务器中执行下面的命令。

curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz

tar zxf lua-5.3.5.tar.gz

cd lua-5.3.5

make linux test

注意:此时安装,有可能会出现如下错误:
在这里插入图片描述
此时需要安装lua相关依赖库的支持,执行如下命令即可:

yum install libtermcap-devel ncurses-devel libevent-devel readline-devel

此时再执行lua测试看lua是否安装成功

[root@localhost ~]# lua
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio

2.5 入门程序

(1) 创建hello.lua文件并编辑

vi hello.lua

(2)在文件中输入print(“hello lua”)后保存并退出。

(3)执行命令并查看输出

lua hello.lua

效果如下:
在这里插入图片描述

2.6 Lua的基本语法

Lua有交互式编程和脚本式编程。

  • 交互式编程:是直接输入语法,通过命令 lua -i 或 lua 来启用

  • 脚本式编程:将Lua程序代码保存到一个以.lua结尾的文件并执行

一般采用脚本式编程

2.6.1 注释

-- 单行注释

--[[
	多行注释
	多行注释
--]]

2.6.2 定义变量

默认的情况下,定义一个变量都是全局变量,如果要用局部变量需要声明为local。

-- 全局变量赋值
a=1
-- 局部变量赋值
local b=2 

如果变量没有初始化:则 它的值为nil 这和java中的null不同。

如下图案例:
在这里插入图片描述

2.6.3 Lua中的数据类型

Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。

Lua 中基本类型如下:

数据类型 描述
nil 只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean false和true
number 双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table 其实是一个"关联数组,数组的索引可以是数字、字符串或表类型

实例:

print(type("Hello world"))      --> string
print(type(10.4*3))             --> number
print(type(print))              --> function
print(type(type))               --> function
print(type(true))               --> boolean
print(type(nil))                --> nil

2.6.4 流程控制

(1)if语句

Lua中 if 语句 由一个布尔表达式作为条件判断,其后紧跟其他语句组成。

语法:

if(布尔表达式)
then
   --[ 在布尔表达式为 true 时执行的语句 --]
end

实例:
在这里插入图片描述
(2)if…else语句

Lua中 if 语句可以与 else 语句搭配使用, 在 if 条件表达式为 false 时执行 else 语句代码块。

语法:

if(布尔表达式)
then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end

实例:
在这里插入图片描述

2.6.5 循环

(1) while循环[满足条件就循环]

Lua 编程语言中 while 循环语句在判断条件为 true 时会重复执行循环体语句。

语法:

while(condition)
do
   statements
end

实例:

a=10
while( a < 20 )
do
   print("a 的值为:", a)
   a = a+1
end

效果如下:
在这里插入图片描述
(2) for循环

Lua 编程语言中 for 循环语句可以重复执行指定语句,重复次数可在 for 语句中控制。

语法:

for var=exp1,exp2,exp3 
do  
    <执行体>  
end  

var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次 "执行体"。exp3 是可选的,如果不指定,默认为1。若想执行递减操作,exp3应为负数

实例:

for i=1,9,2
do
   print(i)
end

i从1开始循环,当循环到 i=9 时停止,每次 i 递增 2
效果如下:在这里插入图片描述
(3)repeat…until语句[满足条件结束]

Lua 编程语言中 repeat…until 循环语句不同于 for 和 while循环,for 和 while 循环的条件语句在当前循环执行开始时判断,而 repeat…until 循环的条件语句在当前循环结束后判断,相当于do while循环

语法:

repeat
   statements
until( condition )

实例:

num =5
repeat
print(num)
num=num-1
until (num==0)

效果如下:
在这里插入图片描述

2.6.6 函数

lua中也可以定义函数,类似于java中的方法。结束需要添加end

实例:

--[[ 函数返回两个值的最大值 --]]
function max(num1, num2)

   if (num1 > num2) then
      result = num1;
   else
      result = num2;
   end

   return result; 
end
-- 调用函数
print("两值比较最大值为 ",max(10,4))
-- ..  表示拼接
print("两值比较最大值为 "..max(5,6))

效果如下:
在这里插入图片描述

2.6.7 表

table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组、字典等。

Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。

实例:

-- 初始化表
mytable = {}

-- 指定值
mytable[1]= "Lua"

-- 移除引用
mytable = nil

2.6.7 模块

(1) 模块定义

模块类似于一个封装库,从 Lua5.1开始,Lua加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

创建一个文件叫module.lua,在module.lua中创建一个独立的模块,代码如下:

-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}
 
-- 定义一个常量
module.constant = "这是一个常量"
 
-- 定义一个函数
function module.func1()
    print("这是一个公有函数")
end
 
local function func2()
    print("这是一个私有函数!")
end
 
function module.func3()
    func2()
end
 
return module

由上可知,模块的结构就是一个 table 的结构,因此可以像操作调用 table 里的元素那样来操作调用模块里的常量或函数。

上面的 func2 声明为程序块的局部变量,即表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的公有函数来调用.

(2) require 函数

require 用于 引入其他的模块,类似于java中的类要引用别的类的效果。

用法:

require("<模块名>")
require "<模块名>"

(3) 应用
将上面定义的module模块引入使用,创建一个test_module.lua文件

-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
print(module.constant)
module.func3()

效果如下:
在这里插入图片描述

3. OpenResty®

3.1 OpenResty介绍

OpenResty® 是一个基于Nginx与Lua的高性能 Web 平台,其内部集成了大量精良的Lua库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty® 通过汇聚各种设计精良的Nginx模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用Lua脚本语言调动Nginx支持的各种C以及Lua模块,快速构造出足以胜任10K乃至1000K以上单机并发连接的高性能 Web 应用系统。

OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

3.2 安装OpenResty

OpenResty提供了各种服务器环境的安装方式,因为我是CentOS, 所以选择了以下命令,其他类型可以到官网查看对应方法。

1.添加仓库执行命令

 yum install yum-utils
 yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

2.执行安装

yum install openresty

3.安装成功后 默认在/usr/local/目录,如下图
在这里插入图片描述

3.3 配置Nginx

默认已经安装好了Nginx,在目录:/usr/local/openresty/nginx 下。

修改/usr/local/openresty/nginx/conf/nginx.conf,将配置文件使用的根设置为root,目的就是将来要使用lua脚本的时候 ,直接可以加载在root下的lua脚本。

vi /usr/local/openresty/nginx/conf/nginx.conf

修改代码如下:
在这里插入图片描述

3.4 测试访问

(1) 修改nginx.conf配置文件

vi /usr/local/openresty/nginx/conf/nginx.conf

添加如下代码
在这里插入图片描述
(2) 启动OpenResty-Nginx服务

--启动
/usr/local/openresty/nginx/sbin/nginx
--停止
/usr/local/openresty/nginx/sbin/nginx -s stop
--重启
/usr/local/openresty/nginx/sbin/nginx -s reload
--检验nginx配置是否正确
/usr/local/openresty/nginx/sbin/nginx -t

(3) 浏览器访问:
http://www.xiexun.top:9100/test
在这里插入图片描述
注意阿里云服务器安全组需要开启9100端口

4. 广告缓存的载入与读取

4.1 逻辑简述

  • 定义请求名称

  • 书写Lua脚本,查询Nginx缓存是否有数据,若无数据,依次查询Redis与MySQL并对数据进行保存

  • 发起请求,获取数据

4.2 功能实现

4.2.1 请求地址

http://www.xiexun.top:9100/read_advert?id=1

4.2.2 脚本编写

在/root/lua目录下创建update_advert.lua脚本

脚本如下:

ngx.header.content_type = "application/json;charset=utf8"
local uri_args = ngx.req.get_uri_args();
local id = uri_args["id"];
--获取本地缓存
local cache_ngx = ngx.shared.dis_cache;
--根据ID 获取本地缓存数据
local advertCache = cache_ngx:get('advert_cache_' .. id);

if advertCache == "" or advertCache == nil then
    ngx.say("本地缓存中无数据");
    ngx.say("开始从Redis中获取数据.....")
    local redis = require("resty.redis");
    local red = redis:new()
    red:set_timeout(10000)
    red:connect("39.105.162.100", 6379)
    local redisAdvert = red:get("advert_" .. id);

    if ngx.null==redisAdvert then
        ngx.say("Redis中无数据");
        ngx.say("开始从数据库中获取数据.....")
        local cjson = require("cjson");
        local mysql = require("resty.mysql");
        local db = mysql:new();
        db:set_timeout(10000)
        local props = {
            host = "39.105.162.100",
            port = 3306,
            database = "changgou_advert",
            user = "root",
            password = "root"
        }
        local res = db:connect(props);
        local select_sql = "select url,pic from tb_advert where status ='1' and category_id=" .. id .. " order by sort_order";
        res = db:query(select_sql);
        ngx.say("数据库查询成功");
        local responsejson = cjson.encode(res);
        red:set("advert_" .. id, responsejson);
        ngx.say(responsejson);
        db:close()
    else
        ngx.say("Redis查询成功")
        cache_ngx:set('advert_cache_' .. id, redisAdvert, 10 * 60);
        ngx.say(redisAdvert)
    end
    red:close()
else
    ngx.say(advertCache)
end

4.2.3 Nginx配置

vi /usr/local/openresty/nginx/conf/nginx.conf

添加如下代码

        location /read_advert {
                limit_req zone=advertRateLimit;
                content_by_lua_file  /root/lua/read_advert.lua;
        }

如下图所示
在这里插入图片描述

4.3 测试

测试地址:http://www.xiexun.top:9100/read_advert?id=1
在这里插入图片描述

5. Nginx限流

一般情况下,首页的并发量是比较大的。当用户不停的刷新首页时,即使是多级缓存,也会产生一定压力。所以需要引入限流来进行保护

5.1 生活中限流对比

  • 水坝泄洪:通过闸口限制洪水流量(控制流量速度)

  • 办理银行业务:所有人先领号,各窗口叫号处理。每个窗口处理速度根据客户具体业务而定,所有人排队等待叫号即可。若快下班时,告知客户明日再来(拒绝流量)

  • 火车站排队买票安检:通过排队的方式依次放入。(缓存带处理任务)

5.2 nginx的限流

(1) Nginx限流的方式:

  • 控制速率:limit_req_zone

  • 控制并发连接数:limit_conn_zone

(2) Nginx限流算法

令牌桶算法

  • 令牌以固定速率产生,并缓存到令牌桶中;

  • 令牌桶放满时,多余的令牌被丢弃;

  • 请求要消耗等比例的令牌才能被处理;

  • 令牌不够时,请求被缓存。

漏桶算法

  • 水(请求)从上方倒入水桶,从水桶下方流出(被处理)

  • 来不及流出的水存在水桶中(缓冲),以固定速率流出

  • 水桶满后水溢出(丢弃)

  • 这个算法的核心是:缓存请求、匀速处理、多余的请求直接丢弃

相比漏桶算法,令牌桶算法不同之处在于它不但有一只“桶”,还有个队列,这个桶是用来存放令牌的,队列才是用来存放请求的。

从作用上来说,漏桶和令牌桶算法最明显的区别就是是否允许突发流量(burst)的处理,漏桶算法能够强行限制数据的实时传输(处理)速率,对突发流量不做额外处理;而令牌桶算法能够在限制数据的平均传输速率的同时允许某种程度的突发传输。

Nginx按请求速率限速模块使用的是漏桶算法,即能够强行保证请求的实时处理速度不会超过设置的阈值。

5.2.1 控制速率

(1) limit_req_zone 参数配置讲解

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
  • $binary_remote_addr:表示通过remote_addr这个标识来做限制,“binary_”的目的是缩写内存占用量,是限制同一客户端IP地址

  • zone=one:10m:表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息

  • rate=1r/s:表示允许相同标识的客户端的访问频次,这里限制的是每秒1次,还可以30r/m

limit_req zone=one burst=5 nodelay;
  • zone=one:表示设置使用哪个配置区域来做限制,与上面limit_req_zone 里的name对应

  • burst=5:表示设置一个大小为5的缓冲区,当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内

  • nodelay:如果设置,超过访问频次而且缓冲区也满了的时候就会直接返回503,如果没有设置,则所有请求会等待排队。

(2) 修改Nginx.conf

代码如下:

user  root root;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    #cache
    lua_shared_dict dis_cache 128m;

    #限流设置
    limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        location /update_content {
            content_by_lua_file /root/lua/update_content.lua;
        }

        location /read_content {
            #使用限流配置
            limit_req zone=contentRateLimit;
            content_by_lua_file /root/lua/read_content.lua;
        }
    }
}

(3) 测试

重新加载配置文件

访问页面:http://www.xiexun.top:9100/read_content?id=1,连续刷新会直接报错。
在这里插入图片描述
(4) 处理突发流量

上面例子限制 2r/s,如果有时正常流量突然增大,超出的请求将被拒绝,无法处理突发流量,可以结合 burst 参数使用来解决该问题。

设置**burst=4 **,若同时有4个请求到达,Nginx 会处理第一个请求,剩余3个请求将放入队列,然后每隔500ms从队列中获取一个请求进行处理。若请求数大于4,将拒绝处理多余的请求,直接返回503.

不过,单独使用 burst 参数并不实用。假设 burst=50 ,rate依然为10r/s,排队中的50个请求虽然每100ms会处理一个,但第50个请求却需要等待 50 * 100ms即 5s,这么长的处理时间自然难以接受。

因此,burst 往往结合 nodelay 一起使用。

例如:如下配置:

server {
    listen       80;
    server_name  localhost;
    location /update_content {
        content_by_lua_file /root/lua/update_content.lua;
    }
    location /read_content {
        limit_req zone=contentRateLimit burst=4 nodelay;
        content_by_lua_file /root/lua/read_content.lua;
    }
}

如上表示:

平均每秒允许不超过2个请求,突发不超过4个请求,并且处理突发4个请求的时候,没有延迟,等到完成之后,按照正常的速率处理。

如上两种配置结合就达到了速率稳定,但突然流量也能正常处理的效果。完整配置代码如下:

user  root root;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    #cache
    lua_shared_dict dis_cache 128m;

    #限流设置
    limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        location /update_content {
            content_by_lua_file /root/lua/update_content.lua;
        }

        location /read_content {
            limit_req zone=contentRateLimit burst=4 nodelay;
            content_by_lua_file /root/lua/read_content.lua;
        }
    }
}

5.2.2 控制并发量

(1) ngx_http_limit_conn_module 参数配置讲解

这个模块用来限制单个IP的请求数。并非所有的连接都被计数。只有在服务器处理了请求并且已经读取了整个请求头时,连接才被计数。

(2) 配置限制固定连接数

配置如下:

http {
    include       mime.types;
    default_type  application/octet-stream;

    #cache
    lua_shared_dict dis_cache 128m;

    #限流设置
    limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;

    #根据IP地址来限制,存储内存大小10M
    limit_conn_zone $binary_remote_addr zone=addr:1m;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;
        #所有以brand开始的请求,访问本地changgou-service-goods微服务
        location /brand {
            limit_conn addr 2;
            proxy_pass http://192.168.211.1:18081;
        }

        location /update_content {
            content_by_lua_file /root/lua/update_content.lua;
        }

        location /read_content {
            limit_req zone=contentRateLimit burst=4 nodelay;
            content_by_lua_file /root/lua/read_content.lua;
        }
    }
}

其中:

limit_conn_zone $binary_remote_addr zone=addr:10m  表示限制根据用户的IP地址来显示,设置存储地址为的内存大小10M

limit_conn addr 2  表示 同一个地址只允许连接2次。

(3) 限制每个客户端IP与服务器的连接数,同时限制与虚拟服务器的连接总数

如下配置:

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m; 
server {  
    listen       80;
    server_name  localhost;
    charset utf-8;
    location / {
        limit_conn perip 10;#单个客户端ip与服务器的连接数.
        limit_conn perserver 100; #限制与服务器的总连接数
        root   html;
        index  index.html index.htm;
    }
}

6. Canal介绍

Canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB)

6.1 工作原理

6.1.1 MySQL主从复制实现

在这里插入图片描述

  • master将改变记录到二进制日志(binary log)中。这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看

  • slave将master的binary log events拷贝到它的中继日志(relay log)

  • slave重做中继日志中的事件,将改变反映它自己的数据

6.1.2 工作原理

在这里插入图片描述

  • canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议

  • mysql master收到dump请求,开始推送binary log给slave(也就是canal)

  • canal解析binary log对象(原始为byte流)

6.2 安装canal

6.2.1 下载镜像

docker canal/canal-server:v1.1.4

6.2.2 启动容器

docker run -p 11111:11111 --name canal -d canal/canal-server:v1.1.4

6.2.3 配置instance.properties

vi canal-server/conf/example/instance.properties

在这里插入图片描述
详细配置可参考Canal AdminGuide

6.2.4 重启canal

docker update --restart=always canal

docker restart canal

6.3 开启binlog模式

在使用canal时,需要开启MySQL的binlog模式。同时要求MySQL的版本为5.7及以下。

6.3.1 修改mysqld.cnf

docker exec -it mysql /bin/bash
cd /etc/mysql/mysql.conf.d
vi mysqld.cnf

添加如下配置:

log-bin /var/lib/mysql/mysql-bin
server-id=12345

6.3.2 创建账号

使用root账号创建用户并授予权限

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;

6.3.3 重启MySQL

docker restart mysql

7. 广告同步实现

当用户执行数据库的操作的时候,binlog日志会被canal捕获到,并解析出数据。然后将解析出来的数据进行同步到Redis中即可。

7.1 canal微服务

7.1.1 搭建canal微服务

thankson-springcloud-provider下创建thankson-springcloud-canal工程,并引入相关配置。

项目结构如下
在这里插入图片描述

7.1.2 pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<?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>
        <artifactId>thankson-springcloud-provider</artifactId>
        <groupId>com.thankson.springcloud</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>thankson-springcloud-canal</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.thankson.springcloud</groupId>
            <artifactId>thankson-springcloud-mall-api-advert</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

7.1.3 application.yml配置

server:
  port: 9100
spring:
  application:
    name: changgou-canal
  redis:
    host: www.xiexun.top
    port: 6379
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true

#canal配置
canal:
  client:
    instances:
      example:
        host: www.xiexun.top
        port: 11111
        clusterEnabled: false
        retryCount: 5

7.1.4 创建监听器

在包com.thankson.canal.listener下创建CanalDataEventListener类,实现对表增删改操作的监听,代码如下:

@CanalEventListener
public class CanalDataEventListener {

    @Autowired
    private AdvertFeign advertFeign;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @ListenPoint(destination = "example",
            schema = "changgou_advert",
            table = {"tb_advert", "tb_advert_category"},
            eventType = {
                    CanalEntry.EventType.UPDATE,
                    CanalEntry.EventType.DELETE,
                    CanalEntry.EventType.INSERT})
    public void onEventCustomUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
        //1.获取列名 为category_id的值
        String categoryId = getColumnValue(eventType, rowData);
        //2.调用feign 获取该分类下的所有的广告集合
        Result<List<Advert>> categoryResult = advertFeign.findByCategory(Integer.valueOf(categoryId));
        List<Advert> data = categoryResult.getData();
        for (Advert datum : data) {
            System.out.println(datum);
        }
        //3.使用redisTemplate存储到redis中
        stringRedisTemplate.boundValueOps("advert_" + categoryId).set(JSON.toJSONString(data));
    }

    private String getColumnValue(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
        String categoryId = "";
        //判断 如果是删除  则获取BeforeColumnsList
        if (eventType == CanalEntry.EventType.DELETE) {
            for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
                if (column.getName().equalsIgnoreCase("category_id")) {
                    categoryId = column.getValue();
                    return categoryId;
                }
            }
        } else {
            //判断 如果是添加 或者是更新 获取AfterColumnsList
            for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
                if (column.getName().equalsIgnoreCase("category_id")) {
                    categoryId = column.getValue();
                    return categoryId;
                }
            }
        }
        return categoryId;
    }
}

7.1.5 启动类创建

com.thankson.canal包下创建启动类CanalApplication,代码如下:

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableCanalClient
@EnableFeignClients(basePackages = {"com.thankson.mall.advert.feign"})
public class CanalApplication {
    public static void main(String[] args) {
        SpringApplication.run(CanalApplication.class,args);
    }
}

7.2 advert微服务

7.2.1 搭建advert微服务

  • thankson-springcloud-mall-service模块下创建thankson-springcloud-mall-service-advert微服务

  • thankson-springcloud-mall-api模块下创建thankson-springcloud-mall-api-advert微服务

项目结构如下
在这里插入图片描述

7.2.2 pom.xml配置

  • api-advert
<?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>
        <artifactId>thankson-springcloud-mall-api</artifactId>
        <groupId>com.thankson.springcloud</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>thankson-springcloud-mall-api-advert</artifactId>
</project>
  • service-advert
<?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>
        <artifactId>thankson-springcloud-mall-service</artifactId>
        <groupId>com.thankson.springcloud</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>thankson-springcloud-mall-service-advert</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.thankson.springcloud</groupId>
            <artifactId>thankson-springcloud-mall-api-advert</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

</project>

7.2.3 application.yml配置

  • service-advert
server:
  port: 10102
spring:
  application:
    name: mall-advert
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://www.xiexun.top:3306/changgou_advert?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: canal
    password: canal
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true

mybatis:
  configuration:
    map-underscore-to-camel-case: true  #开启驼峰功能
hystrix:
  command:
    default:
      execution:
        timeout:
          #如果enabled设置为false,则请求超时交给ribbon控制
          enabled: true
        isolation:
          strategy: SEMAPHORE

7.2.4 业务代码

因为采用脚本生成代码,所以不进行过多的介绍。具体代码可以参考项目源码。

7.2.5 启动类创建

@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.thankson.mall.advert.dao"})
public class AdvertApplication {

    public static void main(String[] args) {
        SpringApplication.run(AdvertApplication.class);
    }
}

7.2.6 测试

1、启动eureka、canal、advert微服务,效果如下
在这里插入图片描述
2、查看Redis数据
在这里插入图片描述
3、修改表tb_advert中广告分类为1的数据后查看redis
在这里插入图片描述
由上图可见 代码已正确执行

8. 结束语

至此,广告同步功能已经完成。至于其他功能在后续使用时会进行开发

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章