高并发高可用的缓存解决方案
OpenResty+lua+redis 实现高并发的数据缓存(二级缓存)读取
OpenResty
-
基于 NGINX 的可伸缩的 Web 平台
-
强大的 Web 应用服务器,可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块
-
安装
#添加仓库 yum install yum-utils yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo # 默认安装目录 /usr/local/openresty yum install openresty #nginx默认目录:/usr/local/openresty/nginx -
修改/usr/local/openresty/nginx/conf/nginx.conf
#user nobody; #使用lua脚本的时直接可以加载在root下的lua脚本 user root root;
编写lua实现数据从mysql加载到redis
-- 设置响应头
ngx.header.content_type="application/json;charset=utf-8"
--获取请求携带的参数
local uri_args = ngx.req.get_uri_args()
--获取指定名称的参数(可通过拼接成指定的redis的key,可拼接到mysql中)
local parameter = uri_args["parameter"]
--引入mysql库
local mysql = require("resty.mysql")
--新建连接
local db = mysql:new()
--设置超时时间
db:set_timeout(2000)
--定义mysql连接数据
local props = {
host="mysql地址",
port=3306,
database="指定数据库",
user="账号",
password="密码"
}
--连接数据库
local res = db:connect(props)
--定义SQL语句
local select_sql = "自定义SQL查询语句"
--执行查询,获得结果
res = db:query(select_sql)
--关闭数据库连接
db:close()
--加载redis库
local redis = require("resty.redis")
--新建redis连接
local redcon = redis:new()
--设置超时时间
redcon:set_timeout(2000)
local ip = "redis服务器地址"
local port = 6379
--连接redis服务器
redcon:connect(ip,port)
--引入cjson模块
local cjsonlocal = require("cjson")
--将数据存入redis
redcon:set("key",cjsonlocal.encode(res))
--关闭连接
redcon:close()
--如果接口调用成功,则返回信息
ngx.say("{status:true}")
编写lua实现通过OpenResty读取redis中的数据
--设置响应头
ngx.header.content_type="application/json;charset=utf-8"
--获取请求参数
local uri_args=ngx.req.get_uri_args()
--获取指定名称的参数(可通过拼接成指定的redis的key)
local parameter=uri_args["parameter"]
--获取OpenResty本地缓存(二级缓存)
local cache_ngx = ngx.shared.my_cache
--根据ID获取本地缓存数据
local adcache = cache_ngx:get("指定key")
if adcache == "" or adcache == nil then
-- 二级缓存中没有数据,前往二级缓存(redis)中读取数据
local redis = require("resty.redis")
local redcon = redis:new()
redcon:set_timeout(2000)
local host = "redis服务器地址"
local port = 6379
local ok,err = red:connect(host,port)
local rescontent = redcon:get("指定的key")
-- 返回数据
ngx.say(rescontent)
redcon:close()
-- 将数据放入二级缓存中,并设置过期时间
cache_ngx:set("key",rescontent,10*60)
else
-- 返回数据
ngx.say(adcache)
end
配置nginx文件(在指定位置添加或者修改)
#user nobody;
#配置当前openresty可以在root加载lua脚本的权限
user root root;
http {
#配置当前openresty本地缓存名字为my_cache,大小为5兆
lua_shared_dict my_cache 5m;
#limit_req_zone开启限流,$binary_remote_addr通过客户端IP限流,zone是定义共享内存区来存储访问信息(名字为myTestLimit,大小10m)
#rate设置最大访问速率,6r/s表示每秒最多处理6个请求
limit_req_zone $binary_remote_addr zone=myTestLimit:10m rate=6r/s;
server {
#添加字符集
charset utf-8;
#设置读取缓存访问
location /readtest {
# 对该路径访问设置名称为myTestLimit的桶限流,最多缓存来自同一个ip的20个请求,多余拒绝,采用多线程不延迟处理方式
limit_req zone=myTestLimit burst=20 nodelay;
# 通过执行指定的lua文件读取数据并返回
# 访问地址 http://(OpenResty服务器ip):(OpenResty服务指定端口号)/readtest?parameter=xxx
content_by_lua_file /root/lua/readtest.lua;
}
#设置更新缓存访问
location /loadtest {
# 通过执行指定的lua文件将数据存入缓存
# 访问地址 http://(OpenResty服务器ip):(OpenResty服务指定端口号)/loadtest?parameter=xxx
content_by_lua_file /root/lua/loadtest.lua;
}
}
}
将lua文件放入OpenResty服务器的/root/lua中
- 调用方式
- http://(OpenResty服务器ip):(OpenResty服务指定端口号)/loadtest?parameter=xxx
- http://(OpenResty服务器ip):(OpenResty服务指定端口号)/readtest?parameter=xxx
以上过程实现了高并发,再通过canal+rabbitMQ实现高可用
canal
-
模拟了mysql slave的交互协议,向mysql master发送dump协议
-
mysql master收到dump请求,推送binary log给canal
-
canal解析binary log对象
-
通过开源项目starter-canal监控数据的变化
- 监听变化实现订阅与消费(MQ)
- 监听变化同步数据到缓存(消费者调用指定接口同步缓存)
- 监听变化实现全文检索数据更新(消费者调用指定接口同步数据到ES)
- 监听变化实现业务逻辑处理(发邮件、发短信...)
-
安装
-
mysql开启binlog模式
-
mysql是否开启binlog模式
SHOW VARIABLES LIKE '%log_bin%' -
开启binlog模式(修改mysql.cnf配置文件)
[mysqld] log-bin=mysql-bin binlog-format=ROW server_id=1
-
-
canal服务端安装配置
-
下载地址canal
https://github.com/alibaba/canal/releases/tag/canal-1.0.24 -
解压缩到指定的目录/usr/local/canal
tar -zxvf () -C /usr/local/canal -
修改配置
vi /usr/local/canal/conf/example/instance.properties canal.instance.master.address = mysql地址:端口 canal.instance.dbUsername = mysql账号 canal.instance.dbPassword = mysql密码 #canal.instance.defaultDatabaseName = 注释掉,防止全表扫描 -
启动服务
cd /usr/local/canal/bin ./startup.sh -
查看日志
tail -100f /usr/local/canal/logs/canal/canal.log #如果出现 the canal server is running now ...... 表示启动成功
-
-
创建监听服务
-
下载starter-canal第三方服务,并通过mvn install 将其安装到本地maven库中
-
创建监听服务,并将数据发送到MQ
-
添加依赖
<dependency> <groupId>com.xpand</groupId> <artifactId>starter-canal</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> -
配置canal服务和MQ服务
canal: client: instances: example: host: # Canal服务端的IP地址 port: 11111 # Canal服务端的端口 batchSize: # 每次批量监听的条数 spring: rabbitmq: host: # rabbitmq服务端的IP地址 port: 5672 virtual-host: # rabbitmq的虚拟机 username: #账号 password: #密码 -
启动类添加注解@EnableCanalClient
@SpringBootApplication @EnableCanalClient public class CanalApplication { public static void main(String[] args) { SpringApplication.run(CanalApplication.class,args); } } -
创建监听类监听指定库指定表的数据变化
@CanalEventListener public class TestListener { @Autowired private RabbitTemplate template; @ListenPoint(schema = "指定数据库", table = {"指定表"}) public void adUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData){ System.out.println("数据产生了变化"); //变化前的数据 rowData.getBeforeColumnsList().forEach((c) -> { System.out.println(c.getName()+"::"+c.getValue()); if ("position".equals(c.getName())) { //将数据发送到rabbitMQ template.convertAndSend("队列名称",c.getValue()); } }); //变化后的数据 rowData.getAfterColumnsList().forEach((c) -> { System.out.println(c.getName()+"::"+c.getValue()); }); } }
-
-
创建MQ的消费者消费数据,调用数据更新接口实现缓存更新
-
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!--Http的客户端--> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>3.9.0</version> </dependency> -
配置rabbitMQ
spring: rabbitmq: host: # rabbitmq服务端的IP地址 port: 5672 virtual-host: # rabbitmq的虚拟机 username: #账号 password: #密码 -
编写监听类,同步更新缓存
@Component @RabbitListener(queues = "监听的队列名称") public class CanalHandle { @RabbitHandler public void tnAdHandler(String msg){ //更新缓存的OpenResty+lua的地址 String url = "http://(OpenResty服务器ip):(OpenResty服务指定端口号)/readtest?parameter="+msg; //创建http请求客户端 OkHttpClient okHttpClient = new OkHttpClient(); //构建Request请求 Request.Builder builder = new Request.Builder(); Request request = builder.url(url).build(); //发送请求 Call call = okHttpClient.newCall(request); //回调请求处理的结构 call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { //更新缓存失败 } @Override public void onResponse(Call call, Response response) throws IOException { //更新缓存成功 } }); } }
-