高并发高可用的缓存解决方案

445 阅读5分钟

高并发高可用的缓存解决方案

OpenResty+lua+redis 实现高并发的数据缓存(二级缓存)读取
OpenResty
  1. 基于 NGINX 的可伸缩的 Web 平台

  2. 强大的 Web 应用服务器,可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块

  3. 安装

    #添加仓库
    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
    
  4. 修改/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
  1. 模拟了mysql slave的交互协议,向mysql master发送dump协议

  2. mysql master收到dump请求,推送binary log给canal

  3. canal解析binary log对象

  4. 通过开源项目starter-canal监控数据的变化

    • 监听变化实现订阅与消费(MQ)
    • 监听变化同步数据到缓存(消费者调用指定接口同步缓存)
    • 监听变化实现全文检索数据更新(消费者调用指定接口同步数据到ES)
    • 监听变化实现业务逻辑处理(发邮件、发短信...)
  5. 安装

    • 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 ......   表示启动成功
        
创建监听服务
  1. 下载starter-canal第三方服务,并通过mvn install 将其安装到本地maven库中

  2. 创建监听服务,并将数据发送到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());
              });
          }
      
      }
      
  3. 创建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 {
                      //更新缓存成功
                  }
              });
          }
      
      }