好好学习,天天向上
本文已收录至我的Github仓库 DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star
- 畅购商城(一):环境搭建
- 畅购商城(二):分布式文件系统FastDFS
- 畅购商城(三):商品管理
- 畅购商城(四):Lua、OpenResty、Canal实现广告缓存与同步
- 畅购商城(五):Elasticsearch实现商品搜索
- 畅购商城(六):商品搜索
- 畅购商城(七):Thymeleaf实现静态页
- 畅购商城(八):微服务网关和JWT令牌
- 畅购商城(九):Spring Security Oauth2
- 畅购商城(十):购物车
- 畅购商城(十一):订单
- 畅购商城(十二):接入微信支付
- 畅购商城(十三):秒杀系统「上」
代码:github.com/RobodLee/changgou
首页广告介绍
流程
在商城的首页,我们会看到很多广告,而很多时候这些广告内容都是固定的,所以每次访问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
简介
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:https://www.runoob.com/lua/lua-tutorial.html
OpenResty
简介
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——>浏览器。
定义Nginx缓存模块
cd /usr/local/openresty/nginx/conf # nginx的配置目录
vi nginx.conf # 编辑nginx的配置文件
在http里面配置Nginx的缓存模块:
Lua脚本
然后,准备好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,让它能够执行该脚本。编辑上面提到的nginx.conf文件,在http.server中添加图中内容
上面一行的意思是有read_content的请求就执行该lua文件。重新加载一下文件。
cd /usr/local/openresty/nginx/sbin # 切换到nginx下的sbin目录中
./nginx -s road # 重新加载文件
最后测试一下:
可以看到,数据正确加载成功了。我在做这个的时候,有一个数据库字段写错了,然后一直不出结果。所以,小伙伴们一定要注意别写错了。
Nginx限流
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; #使用限流配置
最后不要忘了重新加载文件
cd /usr/local/openresty/nginx/sbin # 切换到nginx下的sbin目录中
./nginx -s road # 重新加载文件
控制并发量
控制并发量就是限制一个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
最后,重新加载一下文件即可。
cd /usr/local/openresty/nginx/sbin # 切换到nginx下的sbin目录中
./nginx -s road # 重新加载文件
Canal环境搭建
介绍
Canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据。当数据库发生增删改的时候,会产生一个日志文件,Canal通过读取日志文件,知道哪些数据发生了变化。在这里,我们将更新的数据给到Canal微服务中,然后微服务把数据写到Redis里。
开启binlog模式以及创建MySQL用户
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
安装和配置Canal容器
- 下载镜像
docker pull docker.io/canal/canal-server
- 安装Canal
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
Canal微服务搭建
首先,在changgou-service下创建一个Module叫changgou-service-canal作为我们的微服务工程。创建完成后就可以添加所需的依赖了,但是我们添加的依赖包Maven仓库里面没有,需要手动导入,视频中文件我在资料里面没找到,然后我在网上找了一个,小伙伴们如果需要的话可以点击下载。下载解压后,打开里面的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中数据同步了。
总结
这篇文章介绍了Lua、OpenResty以及Canal,并实现了广告的缓存与同步功能。操作都不难,但是坑非常多,本来以为很快就能完成,结果花了好几天才完成,所以做这个一定要细心。
如果我的文章对你有些帮助,不要忘了点赞,收藏,转发,关注。要是有什么好的意见欢迎在下方留言。让我们下期再见!
DayDayUPgithub.com