第4章 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有交互式编程和脚本式编程。
交互式编程就是直接输入语法,就能执行。
脚本式编程需要编写脚本,然后再执行命令 执行脚本才可以。
一般采用脚本式编程
(1)交互式编程
Lua 交互式编程模式可以通过命令 lua -i 或 lua 来启用:
(2)脚本式编程
将 Lua 程序代码保存到一个以 lua 结尾的文件并执行,该模式称为脚本式编程。如入门程序
2.6.1 注释
-- 单行注释
--[[
多行注释
多行注释
--]]
2.6.2 定义变量
默认的情况下,定义一个变量都是全局变量,如果要用局部变量需要声明为local。
-- 全局变量赋值
a=1
-- 局部变量赋值
local b=2
如果变量没有初始化:则 它的值为nil 这和java中的null不同。
如下图案例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qIrCtuDi-1585063109729)(images\1564436763084.png)]
2.6.3 Lua中的数据类型
Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。
Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。
数据类型 | 描述 |
---|---|
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) 模块定义
模块类似于一个封装库,从 Lua 5.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®介绍
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.1 安装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.2 安装nginx
默认已经安装好了nginx,在目录:/usr/local/openresty/nginx
下。
修改/usr/local/openresty/nginx/conf/nginx.conf
,将配置文件使用的根设置为root,目的就是将来要使用lua脚本的时候 ,直接可以加载在root下的lua脚本。
cd /usr/local/openresty/nginx/conf
vi nginx.conf
修改代码如下:
3.3 测试访问
(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) 测试效果
执行命令
curl http://localhost:9000/
如下图所示:
或者通过浏览器访问:http://服务器公网IP:9000
4.广告缓存的载入与读取
4.1 需求分析
在首页显示广告信息
4.2 Lua+Nginx配置
(1)查询数据放入redis中
需求分析:
1、定义请求用于查询数据库中的数据更新到Redis中。
2、连接MySQL ,按照广告分类ID读取广告列表。
3、连接Redis,将广告信息存入Redis中。
功能实现:
1、请求地址:http://你的公网IP/update_advert?id=1
2、在/root/lua目录下创建update_advert.lua脚本,实现连接MySQL数据库,并将查询到的数据保存到Redis中
脚本如下:
ngx.header.content_type="application/json;charset=utf8"
--引入JSON模块
local cjson = require("cjson")
--引入MySQL模块
local mysql = require("resty.mysql")
-- 获取请求中的参数
local uri_args = ngx.req.get_uri_args()
local id = uri_args["id"]
--创建数据库对象
local db = mysql:new()
--设置超时时间
db:set_timeout(1000)
--数据库配置
local props = {
host = "你的公网IP",
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)
--关闭数据库连接
db:close()
--引入redis模块
local redis = require("resty.redis")
--创建Redis对象
local red = redis:new()
--设置超时时间
red:set_timeout(2000)
--Redis Ip
local ip ="你的公网IP"
--Redis 端口号
local port = 6379
--连接Redis
red:connect(ip,port)
--保存数据至Redis
red:set("content_"..id,cjson.encode(res))
--关闭Redis连接
red:close()
--打印
ngx.say(cjson.encode(res))
3、修改nginx.conf
修改location信息
server {
listen 80;
server_name localhost;
location /update_content {
content_by_lua_file /root/lua/update_advert.lua;
}
}
定义Lua缓存命名空间
lua_shared_dict dis_cache 128m;
效果如下:
测试:调用http://你的公网IP/update_advert?id=1
(2)从Redis中获取数据
需求分析:
1、定义请求用于查询Redis中的数据。
2、通过Lua脚本直接冲Redis中获取数据
功能实现:
1、请求地址:http://你的公网IP/read_advert?id=1
2、在/root/lua目录下创建read_advert.lua脚本,实现从Redis中获取数据
脚本如下
--设置响应头类型
ngx.header.content_type="application/json;charset=utf8"
--获取请求中的参数ID
local uri_args = ngx.req.get_uri_args();
local id = uri_args["id"];
--引入redis库
local redis = require("resty.redis");
--创建redis对象
local red = redis:new()
--设置超时时间
red:set_timeout(2000)
--连接
local ok, err = red:connect("你的公网IP", 6379)
--获取key的值
local rescontent=red:get("content_"..id)
--输出到返回响应中
ngx.say(rescontent)
--关闭连接
red:close()
3、修改nginx.conf
修改location信息
server {
location /read_advert{
content_by_lua_file /root/lua/read_advert.lua;
}
}
测试:调用http://你的公网IP/update_advert?id=1
(3)加入OpenResty本地缓存
如上的方式并没有什么问题,但请求都到Redis的话Redis的压力会很大。所以我们采用多级缓存的方式来减少下游系统的服务压力。
需求分析:
1、查询OpenResty本地缓存,如果有则直接返回缓存中的数据
2、如果本地缓存中没有数据,查询Redis中的数据
3、如果Redis中没有查询到数据,那么便查询数据库中的数据
功能实现:
1、修改read_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
测试地址:http://www.xiexun.top/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;
}
}
}
测试:
重新加载配置文件
访问页面:http://192.168.211.132/read_content?id=1
,连续刷新会直接报错。
(3)处理突发流量
上面例子限制 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 工作原理
(1) mysql主从复制实现
-
master将改变记录到二进制日志(binary log)中。这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看
-
slave将master的binary log events拷贝到它的中继日志(relay log)
-
slave重做中继日志中的事件,将改变反映它自己的数据
(2)Canal的工作原理
-
canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
-
mysql master收到dump请求,开始推送binary log给slave(也就是canal)
-
canal解析binary log对象(原始为byte流)
6.2 开启binlog模式
(1) 连接到mysql中,并修改/etc/mysql/mysql.conf.d/mysqld.cnf 需要开启主从模式,开启binlog模式。
编辑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
(2) 创建账号 用于测试使用,
使用root账号创建用户并授予权限
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
(3)重启mysql容器
docker restart mysql
6.3 canal容器安装
下载镜像:
docker canal/canal-server:v1.1.4
容器安装
docker run -p 11111:11111 --name canal -d canal/canal-server:v1.1.4
进入容器,修改核心配置canal.properties 和instance.properties,canal.properties 是canal自身的配置,instance.properties是需要同步数据的数据库连接配置。
执行代码如下:
docker exec -it canal /bin/bash
cd canal-server/conf/
vi canal.properties
cd example/
vi instance.properties
修改canal.properties的id,不能和mysql的server-id重复,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BUw8atpz-1585063109736)(images\1560814792482.png)]
修改instance.properties,配置数据库连接地址:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OfCapPNF-1585063109737)(images\1560814968391.png)]
这里的canal.instance.filter.regex
有多种配置,如下:
可以参考地址如下:
https://github.com/alibaba/canal/wiki/AdminGuide
mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\)
常见例子:
1. 所有表:.* or .*\\..*
2. canal schema下所有表: canal\\..*
3. canal下的以canal打头的表:canal\\.canal.*
4. canal schema下的一张表:canal.test1
5. 多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)
注意:此过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提取tableName进行过滤)
配置完成后,设置开机启动,并记得重启canal。
docker update --restart=always canal
docker restart canal
6.4 canal微服务搭建
当用户执行 数据库的操作的时候,binlog 日志会被canal捕获到,并解析出数据。我们就可以将解析出来的数据进行同步到Redis中即可。
思路:创建一个canal微服务,获取binlog日志,解析数据,将数据更新到redis中。这样广告的数据就更新了。
6.4.1 安装辅助jar包
后续将会提供。亦可github上下载springboot-start-canal源码进行安装,安装方式自行百度
6.4.1 canal微服务工程搭建
在changgou-service下创建changgou-service-canal工程,并引入相关配置。
pom.xml配置
<?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>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-service-canal</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--canal依赖-->
<dependency>
<groupId>com.xpand</groupId>
<artifactId>starter-canal</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
application.yml配置
server:
port: 18082
spring:
application:
name: canal
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
#canal配置
canal:
client:
instances:
example:
host: 192.168.211.132
port: 11111
(3)监听创建
创建一个CanalDataEventListener类,实现对表增删改操作的监听,代码如下:
package com.changgou.canal.listener;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.xpand.starter.canal.annotation.*;
@CanalEventListener
public class CanalDataEventListener {
/***
* 增加数据监听
* @param eventType
* @param rowData
*/
@InsertListenPoint
public void onEventInsert(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
rowData.getAfterColumnsList().forEach((c) -> System.out.println("By--Annotation: " + c.getName() + " :: " + c.getValue()));
}
/***
* 修改数据监听
* @param rowData
*/
@UpdateListenPoint
public void onEventUpdate(CanalEntry.RowData rowData) {
System.out.println("UpdateListenPoint");
rowData.getAfterColumnsList().forEach((c) -> System.out.println("By--Annotation: " + c.getName() + " :: " + c.getValue()));
}
/***
* 删除数据监听
* @param eventType
*/
@DeleteListenPoint
public void onEventDelete(CanalEntry.EventType eventType) {
System.out.println("DeleteListenPoint");
}
/***
* 自定义数据修改监听
* @param eventType
* @param rowData
*/
@ListenPoint(destination = "example", schema = "changgou_content", table = {"tb_content_category", "tb_content"}, eventType = CanalEntry.EventType.UPDATE)
public void onEventCustomUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
System.err.println("DeleteListenPoint");
rowData.getAfterColumnsList().forEach((c) -> System.out.println("By--Annotation: " + c.getName() + " :: " + c.getValue()));
}
}
(4)启动类创建
在com.changgou中创建启动类,代码如下:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableCanalClient
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class,args);
}
}
(5)测试
启动canal微服务,然后修改任意数据库的表数据,canal微服务后台输出如下:
6.5 广告同步
如上图,每次执行广告操作的时候,会记录操作日志到,然后将操作日志发送给canal,canal将操作记录发送给canal微服务,canal微服务根据修改的分类ID调用content微服务查询分类对应的所有广告,canal微服务再将所有广告存入到Redis缓存。
6.5.1 content微服务搭建
在changgou-service中搭建changgou-service-content微服务,对应的dao、service、controller、pojo由代码生成器生成。
首先在changgou-service-api中创建changgou-service-content-api,将pojo拷贝到API工程中,如下图:
(1)pom.xml配置
<?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>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-service-content</artifactId>
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-content-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(2)application.yml配置
server:
port: 18084
spring:
application:
name: content
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/changgou_content?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰功能
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
(3)启动类创建
@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.changgou.content.dao"})
public class ContentApplication {
public static void main(String[] args) {
SpringApplication.run(ContentApplication.class);
}
}
6.5.2 广告查询
在content微服务中,添加根据分类查询广告。
(1)业务层
修改changgou-service-content的com.changgou.content.service.ContentService接口,添加根据分类ID查询广告数据,代码如下:
/***
* 根据categoryId查询广告集合
* @param id
* @return
*/
List<Content> findByCategory(Long id);
修改changgou-service-content的com.changgou.content.service.impl.ContentServiceImpl接口实现类,添加根据分类ID查询广告数据,代码如下:
/***
* 根据分类ID查询
* @param id
* @return
*/
@Override
public List<Content> findByCategory(Long id) {
Content content = new Content();
content.setCategoryId(id);
content.setStatus("1");
return contentMapper.select(content);
}
(2)控制层
修改changgou-service-content的com.changgou.content.controller.ContentController,添加根据分类ID查询广告数据,代码如下:
/***
* 根据categoryId查询广告集合
*/
@GetMapping(value = "/list/category/{id}")
public Result<List<Content>> findByCategory(@PathVariable Long id){
//根据分类ID查询广告集合
List<Content> contents = contentService.findByCategory(id);
return new Result<List<Content>>(true,StatusCode.OK,"查询成功!",contents);
}
(3)feign配置
在changgou-service-content-api工程中添加feign,代码如下:
@FeignClient(name="content")
@RequestMapping(value = "/content")
public interface ContentFeign {
/***
* 根据分类ID查询所有广告
*/
@GetMapping(value = "/list/category/{id}")
Result<List<Content>> findByCategory(@PathVariable Long id);
}
6.5.3 同步实现
在canal微服务中修改如下:
(1)配置redis
修改application.yml配置文件,添加redis配置,如下代码:
(2)启动类中开启feign
修改CanalApplication,添加@EnableFeignClients
注解,代码如下:
(3)同步实现
修改监听类CanalDataEventListener,实现监听广告的增删改,并根据增删改的数据使用feign查询对应分类的所有广告,将广告存入到Redis中,代码如下:
上图代码如下:
@CanalEventListener
public class CanalDataEventListener {
@Autowired
private ContentFeign contentFeign;
//字符串
@Autowired
private StringRedisTemplate stringRedisTemplate;
//自定义数据库的 操作来监听
//destination = "example"
@ListenPoint(destination = "example",
schema = "changgou_content",
table = {"tb_content", "tb_content_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<Content>> categoryresut = contentFeign.findByCategory(Long.valueOf(categoryId));
List<Content> data = categoryresut.getData();
//3.使用redisTemplate存储到redis中
stringRedisTemplate.boundValueOps("content_" + categoryId).set(JSON.toJSONString(data));
}
private String getColumnValue(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
String categoryId = "";
//判断 如果是删除 则获取beforlist
if (eventType == CanalEntry.EventType.DELETE) {
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
if (column.getName().equalsIgnoreCase("category_id")) {
categoryId = column.getValue();
return categoryId;
}
}
} else {
//判断 如果是添加 或者是更新 获取afterlist
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
if (column.getName().equalsIgnoreCase("category_id")) {
categoryId = column.getValue();
return categoryId;
}
}
}
return categoryId;
}
}