在商城的首页,我们会看到很多广告,而很多时候这些广告内容都是固定的,所以每次访问MySQL获取广告内容效率是非常低的,比较好的做法就是用Redis和OpenResty做多级缓存。如果缓存中有数据就访问缓存,没有的话再去MySQL中获取,可以大大提高性能。
广告的数据是存放在changgou- content数据库中(我的这份资料里面没有这个数据库,我就自己创建了一个)。里面有两张表,一张是tb_content_catrgory(广告分类表),根据页面的不同位置,广告有不同的分类,比如首页轮播,猜你喜欢等;另一张是tb_content(广告表),这张表里存放了广告的数据。
CREATE TABLE `tb_content_category` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '类目ID', `name` VARCHAR(50) DEFAULT NULL COMMENT '分类名称', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='内容分类'; INSERT INTO `tb_content_category` VALUES (1, '首页轮播广告'); INSERT INTO `tb_content_category` VALUES (2, '今日推荐A'); INSERT INTO `tb_content_category` VALUES (3, '活动专区'); INSERT INTO `tb_content_category` VALUES (4, '猜你喜欢'); CREATE TABLE `tb_content` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `category_id` BIGINT(20) NOT NULL COMMENT '内容类目ID', `title` VARCHAR(200) DEFAULT NULL COMMENT '内容标题', `url` VARCHAR(500) DEFAULT NULL COMMENT '链接', `pic` VARCHAR(300) DEFAULT NULL COMMENT '图片绝对路径', `status` VARCHAR(1) DEFAULT NULL COMMENT '状态,0无效,1有效', `sort_order` INT(11) DEFAULT NULL COMMENT '排序', PRIMARY KEY (`id`), KEY `category_id` (`category_id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; INSERT INTO `tb_content` VALUES (1, 1, '微信广告', 'https://blog.csdn.net/weixin_43461520', 'https://gitee.com/RobodLee/image_store/raw/master/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7.png', '1', 1);
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
cd /usr/local/server # 切换到想要下载的目录,随意 curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz # 下载Lua5.3.5 tar zxf lua-5.3.5.tar.gz # 解压 cd lua-5.3.5 # 切换到解压后的目录 make linux test # 安装 ------------------------------------------------------------------------------------- [root@localhost lua-5.3.5]# lua # 输入lua,出现下面一行说明安装成功 Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
Lua有交互式编程和脚本式编程两种方式。
交互式编程
交互式编程是输入lua命令后进入到lua控制台,然后输入lua命令来执行。
[root@localhost lua-5.3.5]# lua Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio > print("Hello World!") Hello World! >
脚本式编程
脚本式编程就是创建一个.lua文件,然后输入命令“lua filename.lua”来执行。
参考菜鸟教程Lua
OpenResty 是一个强大的 Web 应用服务器,Web 开发人员可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,更主要的是在性能方面,OpenResty可以快速构造出足以胜任 10K 以上并发连接响应的超高性能 Web 应用系统。就是封装了Nginx,并且集成了Lua脚本,开发人员只需要简单地使用提供的模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写lua的脚本,再进行调用了。
yum install yum-utils # 安装yum-utils,为了使用下面一行的命令 # 添加openresty的仓库,不配置这一行安装不了 yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo yum install openresty # 安装openresty,界面会有提示,一路按y就可以了
安装完成之后,不要忘了启动
service openresty start
启动完成之后,用浏览器访问安装了openresty的虚拟机,如果出现了欢迎界面就说明安装成功了。
虽然现在已经可以访问到OpenResty了,但是为了能够直接加载到root目录下的lua脚本,还需要配置一下。
cd /usr/local/openresty/nginx/conf # 切换到openresty安装目录下的nginx目录中的conf目录中 vi nginx.conf #编辑nginx的配置文件
本节的任务就是:Nginx拦截http://192.168.31.200/read_content?id=1,执行Lua脚本,先从Nginx缓存中加载,没有的话就从Redis中加载,再没有的话就从MySQL中加载,然后MySQL——>Redis——>Nginx——>浏览器。
cd /usr/local/openresty/nginx/conf # nginx的配置目录 vi nginx.conf # 编辑nginx的配置文件
在http里面配置Nginx的缓存模块 :
然后,准备好lua脚本, 在root/lua目录下创建一个read_content.lua文件 ,填入以下内容:
ngx.header.content_type="application/json;charset=utf8" local uri_args = ngx.req.get_uri_args(); -- 获取uri中的所有参数 local id = uri_args["id"]; -- 获取名为id的参数 --获取本地缓存 local cache_ngx = ngx.shared.dis_cache; -- 加载Nginx缓存模块,需要先定义 --根据ID 获取本地缓存数据 local contentCache = cache_ngx:get('content_cache_'..id); --[[ Nginx中有缓存就输出缓存,没有的话就从Redis中加载 --]] if contentCache == "" or contentCache == nil then local redis = require("resty.redis"); -- 依赖Redis模块 local red = redis:new() -- 创建Redis对象 red:set_timeout(2000) -- 超时 red:connect("192.168.31.200", 6379) -- 连接Redis local rescontent=red:get("content_"..id); -- 从Redis中读数据 -- Redis中没有就从MySQL中加载 if ngx.null == rescontent then local cjson = require("cjson"); -- 依赖json模块 local mysql = require("resty.mysql"); -- 依赖mysql模块 local db = mysql:new(); -- 创建mysql对象 db:set_timeout(2000) -- 设置过期时间 -- mysql的参数信息 local props = { host = "192.168.31.200", port = 3306, database = "changgou_content", user = "root", password = "root" } local res = db:connect(props); -- 连接mysql local select_sql = "select url,pic from tb_content where status ='1' and category_id="..id.." order by sort_order"; res = db:query(select_sql); --执行sql local responsejson = cjson.encode(res); -- 将mysql返回的数据转换成json red:set("content_"..id,responsejson); -- 存到Redis中 ngx.say(responsejson); -- 输出 db:close() -- 关闭mysql连接 else cache_ngx:set('content_cache_'..id, rescontent, 10*60); -- 把Redis中的数据写到Nginx缓存中,设置过期时间 ngx.say(rescontent) -- 输出 end red:close() -- 关闭Redis连接 else ngx.say(contentCache) -- 输出 end
现在需要配置一下nginx,让它能够执行该脚本。 编辑上面提到的nginx.conf文件,在http.server中添加图中内容 👇 上面一行的意思是有read_content的请求就执行该lua文件。 重新加载一下文件 。
cd /usr/local/openresty/nginx/sbin # 切换到nginx下的sbin目录中 ./nginx -s road # 重新加载文件
最后测试一下: 可以看到,数据正确加载成功了。我在做这个的时候,有一个数据库字段写错了,然后一直不出结果。所以,小伙伴们一定要注意别写错了。
Nginx限流的方式有两种,一种是控制速率,另一种是控制并发量。
控制速率就是限制访问Nginx的数量,如果数量超过限制,就直接拒绝请求,不去处理。 首先我们需要进行一个 限流的配置 ,编辑上面提到的nginx的配置文件,在http里面添加以下内容:
#限流设置 #binary_remote_addr 是一种key,表示基于 remote_addr(客户端IP) 来做限流,binary_ 的目的是压缩内存占用量。 #zone:定义共享内存区来存储访问信息, contentRateLimit:10m 表示一个大小为10M,名字为contentRateLimit的内存区域。1M能存储16000 IP地址的访问信息,10M可以存储16W IP地址访问信息。 #rate 用于设置最大访问速率,rate=10r/s 表示每秒最多处理10个请求。Nginx 实际上以毫秒为粒度来跟踪请求信息,因此 10r/s 实际上是限制:每100毫秒处理一个请求。这意味着,自上一个请求处理完后,若后续100毫秒内又有请求到达, limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;
配置完之后我们还需要 使用限流配置 ,在nginx的配置文件中,在http.server.location中使用限流配置
#burst相当于队列,若rate=2r/s同时有4个请求到达,Nginx 会处理第一个请求,剩余3个请求将放入队列,然后每隔500ms从队列中获取一个请求进行处理。若请求数大于4,将拒绝处理多余的请求,直接返回503 #nodelay,配合burst使用,并发处理不延迟,不按(1s/rate)秒/个的速率处理,等到完成之后,按照正常的速率处理 limit_req zone=contentRateLimit burst=4 nodelay; #使用限流配置
最后不要忘了 重新加载文件
控制并发量就是限制一个ip对服务器的连接数。首先我们需要配置一下, 编辑nginx.conf文件,在http下添加如下配置 。
#根据IP地址来限制,存储内存大小10M,配置名为perip,大小为1m limit_conn_zone $binary_remote_addr zone=perip:10m; #根据IP地址来限制,存储内存大小10M,配置名为perserver,大小为1m limit_conn_zone $server_name zone=perserver:10m;
配置完成之后,我们需要让某一个location使用这个配置,这里,我们让/brand使用这个配置, 在nginx.conf中的http.server.location /brand中添加以下内容 。
limit_conn perip 10; #设置单个客户端ip与服务器的连接数为10. limit_conn perserver 100; #限制与服务器的总连接数为100 #表示这个请求给180主机处理,因为程序运行在主机上,不在虚拟机上 proxy_pass http://192.168.31.180:18081
最后,重新加载一下文件即可。
Canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据。当数据库发生增删改的时候,会产生一个日志文件,Canal通过读取日志文件,知道哪些数据发生了变化。在这里,我们将更新的数据给到Canal微服务中,然后微服务把数据写到Redis里。
Canal是基于mysql的主从模式实现的,模拟了mysql slave的交互协议,把自己伪装成mysql slave,向mysql master发送dump请求,mysql master收到dump请求,开始推送binary log给slave(也就是canal)canal解析binary log对象(原始为byte流)。所以, MySQL必须得开启binlog模式 。
docker exec -it mysql /bin/bash # 进入到mysql中 cd /etc/mysql/mysql.conf.d #切换到 mysql.conf.d文件夹中 vi mysqld.cnf # 编辑 mysqld.cnf文件
因为Canal需要访问数据库,所以我们 给它安排个账号 ,用root账户不太安全。 打开Navicat或者直接命令行,运行 👇
-- 用户名是canal,%表示能在任意机器上登录,密码是123456 -- SELECT查询权限,REPLICATION SLAVE, REPLICATION CLIENT主从复制权限, -- SUPER ON *.* TO 'canal'@'%':用户canal拥有任意数据库,任意表的这些权限 -- FLUSH PRIVILEGES:刷新权限 create user canal@'%' IDENTIFIED by '123456'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;
最后 重启一下MySQL容器 。
docker restart mysql
docker pull docker.io/canal/canal-server
docker run -p 11111:11111 --name canal -d docker.io/canal/canal-server #11111:11111:端口映射
安装完成之后还需要进行配置,
docker exec -it canal /bin/bash # 进入到canal容器中 cd canal-server/conf # 切换到配置文件所在的目录
进入到canal.properties中看看,里面配置了Canal的id,端口等信息👇 再来看看instance.properties,这个文件配置了数据库相关的信息👇 配置完成后,设置开机启动,并记得重启canal。
docker update --restart=always canal docker restart canal
首先, 在changgou-service下创建一个Module叫changgou-service-canal作为我们的微服务工程 。创建完成后就可以添加所需的依赖了,但是我们添加的依赖包Maven仓库里面没有,需要 手动导入 ,视频中文件我在资料里面没找到,然后我在网上找了一个,小伙伴们如果需要的话可以点击下载%EF%BC%9ALua%E3%80%81OpenResty%E3%80%81Canal%E5%AE%9E%E7%8E%B0%E5%B9%BF%E5%91%8A%E7%BC%93%E5%AD%98%E4%B8%8E%E5%90%8C%E6%AD%A5/spring- boot-starter-canal-master.zip)。下载解压后,打开里面的starter-canal目录,在这个目录下打开控制台,使用mvn install命令进行安装,过程可能有点慢,耐心等待即可。 安装完成后就可以 导入这个依赖 了。
<dependencies> <!--canal依赖--> <dependency> <groupId>com.xpand</groupId> <artifactId>starter-canal</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies>
微服务怎么能少了 启动类和配置文件 呢👇
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class}) @EnableEurekaClient @EnableCanalClient public class CanalApplication { public static void main(String[] args) { SpringApplication.run(CanalApplication.class,args); } } server: port: 18083 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.31.200 port: 11111
启动一下,看看有没有问题 哎呀,出问题了。这个问题搞得我一夜都没睡好觉,非常难受,折腾了很长时间。最后把Canal卸了重装,终于搞定了。虚拟机里面有Canal,我就没装直接改改配置就用了,可能是之前哪个地方的配置有问题吧,所以还是得自己装一遍。
再启动一次 终于好了!Canal微服务搭建成功!
如上图,每次执行广告操作的时候,MySQL会记录操作日志,然后将操作日志发送给canal,canal将操作记录发送给canal微服务,canal微服务根据修改的分类ID调用content微服务查询分类对应的所有广告,canal微服务再将所有广告存入到Redis缓存。
首先,我们需要搭建一个广告微服务, 在changgou-service-api中创建一个Module叫changgou-service-content- api 作为API工程,然后 在com.robod.content.pojo包中准备添加两个JavaBean:Content.java和ContentCategory.java 。 然后 在changgou-service下创建一个changgou-service-content工程 作为广告微服务。添加所需的依赖:
<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>
最后,添加配置文件和启动类
server: port: 18084 spring: application: name: content datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.31.200:3306/changgou_content?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: root 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 logging: level: com: debug # 不加这个MyBatis Log插件不打印sql语句 @SpringBootApplication @EnableEurekaClient @MapperScan(basePackages = {"com.robod.mapper"}) public class ContentApplication { public static void main(String[] args) { SpringApplication.run(ContentApplication.class); } }
启动一下👉
这一步需要完成的功能就是根据广告的分类id去查询出对应的广告集合,所以,我们添加一个叫findByCategoryId的方法去实现这个功能,在各层中实现一下。
/** * 根据分类的ID 获取该分类下的所有的广告的列表 * Controller层 ContentController.java */ @GetMapping(value = "/list/category/{id}") public Result<List<Content>> findByCategoryId(@PathVariable long id){ List<Content> contents = contentService.findByCategoryId(id); return new Result<>(true,StatusCode.OK,"成功查询出所有的广告",contents); } ----------------------------------------------------------------------------- //Service层 ContentServiceImpl.java @Override public List<Content> findByCategoryId(long id) { return contentMapper.findByCategoryId(id); } ------------------------------------------------------------------------------- // Dao层 ContentMapper.java @Select("select * from tb_content where category_id = #{id} and status = 1") List<Content> findByCategoryId(long id);
因为我们需要在Canal微服务中调用广告微服务中的方法,所以 在changgou-service-content-api工程中添加feign :
@FeignClient(name="content") //指定微服务的名字 @RequestMapping(value = "/content") public interface ContentFeign { /** * 根据分类ID查询所有广告 * @param id * @return */ @GetMapping(value = "/list/category/{id}") Result<List<Content>> findByCategoryId(@PathVariable long id); }
既然是将数据同步到Redis,那么就需要配置一下Redis, 在Canal微服务中修改application.yml配置文件,添加redis配置 。 接下来,在启动类中开启feign, 修改CanalApplication,添加@EnableFeignClients注解
//要先在changgou-servie-canal中添加changgou-service-content-api的依赖 @EnableFeignClients(basePackages = {"com.robod.content.feign"})
最后, 在com.robod.canal包中添加一个监听类CanalDataEventListener 去监听数据变化并将变化的数据写到Redis中。
/** * @author Robod * @date 2020/7/14 10:47 * 实现MySQL数据监听 */ @CanalEventListener public class CanalDataEventListener { private final ContentFeign contentFeign; private final StringRedisTemplate stringRedisTemplate; public CanalDataEventListener(ContentFeign contentFeign, StringRedisTemplate stringRedisTemplate) { this.contentFeign = contentFeign; this.stringRedisTemplate = stringRedisTemplate; } /** * 监听数据变化,将数据写到Redis中 * @param eventType * @param rowData */ @ListenPoint( destination = "example", schema = "changgou_content", table = {"tb_content","tb_content_category"}, eventType = { CanalEntry.EventType.INSERT, CanalEntry.EventType.UPDATE, CanalEntry.EventType.DELETE} ) public void onEventListener(CanalEntry.EventType eventType, CanalEntry.RowData rowData) { String categoryId = getColumnValue(eventType,rowData); List<Content> contents = contentFeign.findByCategoryId(Long.parseLong(categoryId)).getData(); stringRedisTemplate.boundValueOps("content_"+categoryId).set(JSON.toJSONString(contents)); } private String getColumnValue(CanalEntry.EventType eventType, CanalEntry.RowData rowData) { if (eventType == CanalEntry.EventType.UPDATE || eventType == CanalEntry.EventType.INSERT) { for (CanalEntry.Column column : rowData.getAfterColumnsList()) { if ("category_id".equalsIgnoreCase(column.getName())) { return column.getValue(); } } } if (eventType == CanalEntry.EventType.DELETE) { for (CanalEntry.Column column : rowData.getBeforeColumnsList()) { if ("category_id".equalsIgnoreCase(column.getName())) { return column.getValue(); } } } return ""; } }
来测试一下👇 OK!数据库中和Redis中数据同步了。
原文链接:https://blog.csdn.net/weixin_43461520/article/details/107348436