高并发 -- 多级缓存

9 篇文章 0 订阅

 

 JVM 进程缓存

 

 

@SpringBootTest
public class CaffeineTest {

    @Test
    void test() {
        //构建cache对象
        Cache<String, String> cache = Caffeine.newBuilder().build();
        //存数据
        cache.put("gf", "lb");

        //取数据
        String gf = cache.getIfPresent("gf");
        System.out.println(gf);

        //取取数据,如果未命中,查询数据库
        String defaultGF = cache.get("defaultGF", key -> {
            //根据key,去查询数据库
            return "hana";
        });

        System.out.println(defaultGF);
    }

    @Test
    void maxSize() throws InterruptedException {
        //构建cache对象,最大存储数量为1,如果存入多个,不会立刻删除
        Cache<String, String> cache = Caffeine.newBuilder().maximumSize(1).build();
        cache.put("gf1", "gf1");
        cache.put("gf2", "gf2");
        cache.put("gf3", "gf3");

        Thread.sleep(10L);

        System.out.println(cache.getIfPresent("gf1"));
        System.out.println(cache.getIfPresent("gf2"));
        System.out.println(cache.getIfPresent("gf3"));
    }

    @Test
    void expire() throws InterruptedException {
        //构建cache对象,写入后多长时间失效
        Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(1L, TimeUnit.SECONDS).build();
        cache.put("gf1", "gf1");
        System.out.println(cache.getIfPresent("gf1"));

        Thread.sleep(1200L);

        System.out.println(cache.getIfPresent("gf1"));
    }
}

 实现进程缓存

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, User> userCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }

    @Bean
    public Cache<Long, Blog> blogCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }
}

@Component
public class CaffeineService {
    @Autowired
    private Cache<Long, User> userCache;

    @Autowired
    private Cache<Long, Blog> blogCache;


    public User getUser(Long id) {
        //当缓存中没有,则查询数据库,查到结果放入缓存。减少数据库交互,提供并发量
        return userCache.get(id, key -> {
            //select * from user where id = key
            return new User("lb", 32);
        });
    }

    public Blog getBlog(Long id) {
        return blogCache.get(id, key -> {
            //select * from blog where id = key
            return new Blog();
        });
    }
}

Lua 语言

Lua语言可以在nginx中开发使用的语言

 

 

 

 

 

多级缓存

openResty

 安装

1.安装开发库

首先要安装OpenResty的依赖开发库,执行命令:

yum install -y pcre-devel openssl-devel gcc --skip-broken

2.安装OpenResty仓库

你可以在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum check-update 命令)。运行下面的命令就可以添加我们的仓库:

yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

如果提示说命令不存在,则运行:

yum install -y yum-utils 

然后再重复上面的命令

3.安装OpenResty

然后就可以像下面这样安装软件包,比如 openresty

yum install -y openresty

4.安装opm工具

opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。

如果你想安装命令行工具 opm,那么可以像下面这样安装 openresty-opm 包:

yum install -y openresty-opm

5.目录结构

默认情况下,OpenResty安装的目录是:/usr/local/openresty

 看到里面的nginx目录了吗,OpenResty就是在Nginx基础上集成了一些Lua模块。

6.配置nginx的环境变量

打开配置文件:

vi /etc/profile

在最下面加入两行:

export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH

NGINX_HOME:后面是OpenResty安装目录下的nginx的目录

然后让配置生效:

source /etc/profile

启动和运行

OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致:

 所以运行方式与nginx基本一致:

# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop

nginx的默认配置文件注释太多,影响后续编辑,这里将nginx.conf中的注释部分删除,保留有效部分。

修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:

#user  nobody;
worker_processes  1;
error_log  logs/error.log;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8081;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

在Linux的控制台输入命令以启动nginx:

nginx

然后访问页面:http://192.168.150.101:8081,注意ip地址替换为你自己的虚拟机IP:

 请注意这里的nginx作用是下图中本地缓存nginx,不是反向代理的nginx

openResty使用

在反向代理的Nginx服务上已经配置 /api 的url将路由到 openResty的Nginx服务上

 openResty配置

1.修改nginx.comf文件

#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  
		location /api/item {
            default_type   application/json;
            content_by_lua_file lua/item.lua;
        }

2.编写item.lua文件

ngx.say('{"id":10001,"name":"Phone"}')

 3.openResty 获取请求参数

 

local id = ngx.var[1]
-- ..是lua中的拼接符号
ngx.say('{"id":' .. id .. ',"name":"Phone"}')

OpenResty发送请求到一台JVM 服务器

 

 

 实现:

nginx.conf

		location /item {
            proxy_pass http://192.168.150.1:8081;
        }

 封装http请求工具类

 common.lua

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M

其中会用到cjson工具库

 lua/item.lua

--导入common函数库
local common = require('commnon')
local read_http = common.read_http

--导入cjson函数库,
local cjson = require('cjson')

--获取路径参数
local id = ngx.var[1]

--查询商品信息 nil代表无参
local itemJSON = read_http("/item/" .. id, nil)
--查询库存信息
local stockJSON = read_http("/item/stock/" .. id, nil)

--转化为lua的table类型
--JSON转table则需要cjson工具包
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)

--组合数据 商品的库存(stock)和销量(sold),存储在stock对象中
item.stock = stock.stock
item.sold = stock.sold

--返回结果
ngx.say(cjson.encode(item))

OpenResty发送请求到多台JVM 服务器(负载均衡)

 但是如果存在多台JVM服务器时,缓存只存在上一次接受请求的JVM服务器上,其他的JVM中没有JVM缓存。如果JVM服务器数量很多,并且负载均衡的策略为轮训时,则依旧会造成对数据库的大量访问。则可以对request的uri进行哈希算法,让同一个id永远只会路由到同一台JVM服务器上使其JVM缓存永远生效.

修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:


#user  nobody;
worker_processes  1;
error_log  logs/error.log;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
	#lua 模块
	lua_package_path "/usr/local/openresty/lualib/?.lua;;";
	#c模块     
	lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  

	upstream tomcat-cluster {
		hash $request_uri;
		server 192.168.150.1:8081;
		server 192.168.150.1:8082;
	}
	
    server {
        listen       8081;
        server_name  localhost;
		location /item {
            proxy_pass http://tomcat-cluster;
        }
		location ~ /api/(\d+) {
            default_type   application/json;
            content_by_lua_file lua/item.lua;
        }
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

OpenResty 请求Redis获取数据

 

 

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    public void afterPropertiesSet() throws Exception {
        //初始化缓存
        //查询热点数据存储到Redis中使用json
        //查询热点数据的库存和销量到Redis中使用json
    }
}

下图中导入的Redis工具类"resty.redis"表示在lua文件夹下的resty文件夹下的redis.lua文件 

 

 common.lua

--导入redis
local redis = require('resty.redis')
--初始化redis
local red = redis:new()
red:set_timeouts(1000,1000,1000)

-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
    local pool_size = 100 --连接池大小
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
    end
end

-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)
    -- 获取一个连接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "连接redis失败 : ", err)
        return nil
    end
    -- 查询redis
    local resp, err = red:get(key)
    -- 查询失败处理
    if not resp then
        ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
    end
    --得到的数据为空处理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
    end
    close_redis(red)
    return resp
end

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {
    read_http = read_http,
    read_redis = read_redis
}
return _M

则修改lua/item.lua的逻辑为先查询Redis,未查到在查JVM服务器

--导入common函数库
local common = require('commnon')
local read_http = common.read_http
local read_redis = common.read_redis

--导入cjson函数库,
local cjson = require('cjson')

local function read_data(key, path, params)
    --查询redis
    local resp = read_redis('192.168.99.100', 6379, key)
    --判断查询结果
    if not resp then
        ngx.log('redis查询失败,尝试查询http,key:', key)
        resp = read_http(path, params)
    end
    return resp
end

--获取路径参数
local id = ngx.var[1]

--查询商品信息 nil代表无参
local itemJSON = read_data('item:id:' .. id, "/item/" .. id, nil)
--查询库存信息
local stockJSON = read_data('item:stock:id:' .. id, "/item/stock/" .. id, nil)

--转化为lua的table类型
--JSON转table则需要cjson工具包
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)

--组合数据 商品的库存(stock)和销量(sold),存储在stock对象中
item.stock = stock.stock
item.sold = stock.sold

--返回结果
ngx.say(cjson.encode(item))

OpenResty 本地缓存

 

 修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:

#user  nobody;
worker_processes  1;
error_log  logs/error.log;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
	#lua 模块
	lua_package_path "/usr/local/openresty/lualib/?.lua;;";
	#c模块     
	lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
	#添加共享词典,本地缓存
	lua_shared_dict item_cache 150m;

	upstream tomcat-cluster {
		hash $request_uri;
		server 192.168.150.1:8081;
		server 192.168.150.1:8082;
	}
	
    server {
        listen       8081;
        server_name  localhost;
		location /item {
            proxy_pass http://tomcat-cluster;
        }
		location ~ /api/(\d+) {
            default_type   application/json;
            content_by_lua_file lua/item.lua;
        }
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

item.lua

--导入common函数库
local common = require('commnon')
local read_http = common.read_http
local read_redis = common.read_redis

--导入cjson函数库,
local cjson = require('cjson')

--导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache

local function read_data(key, path, params, expire)
    --查询本地缓存
    local resp = item_cache:get(key);
    if not resp then
        ngx.log(ngx.ERR, '本地缓存查询失败,尝试查询redis , key:', key)
        --查询redis
        resp = read_redis('192.168.99.100', 6379, key)
        --判断查询结果
        if not resp then
            ngx.log(ngx.ERR, 'redis查询失败,尝试查询http , key:', key)
            resp = read_http(path, params)
        end
        --查询成功,把数据写入本地缓存
        item_cache:set(key, resp, expire)
    end
    return resp
end

--获取路径参数
local id = ngx.var[1]

--查询商品信息 nil代表无参
local itemJSON = read_data('item:id:' .. id, "/item/" .. id, nil, 1800)
--查询库存信息
local stockJSON = read_data('item:stock:id:' .. id, "/item/stock/" .. id, nil, 60)

--转化为lua的table类型
--JSON转table则需要cjson工具包
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)

--组合数据 商品的库存(stock)和销量(sold),存储在stock对象中
item.stock = stock.stock
item.sold = stock.sold

--返回结果
ngx.say(cjson.encode(item))

 缓存同步

 

 

Canal

 

canal

优点:,对代码没有任何耦合性,直接监控mysql的binary log在触发时间,异步MQ依然会存在少许的藕合。

缺点:需要Mysql数据库开启主从

 安装 、启动

1.开启Mysql主从

Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。

这里以之前用Docker运行的mysql为例:

1.1开启binlog

打开mysql容器挂载的日志文件,我的在/tmp/mysql/conf目录:

修改文件:

vi /tmp/mysql/conf/my.cnf

 添加内容:

log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima

配置解读:

  • log-bin=/var/lib/mysql/mysql-bin:设置binary log文件的存放地址和文件名,叫做mysql-bin

  • binlog-do-db=heima:指定对哪个database记录binary log events,这里记录heima这个库

最终效果:

skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima

1.2设置用户权限

接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对test这个库的操作权限.

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;

重启mysql容器即可

docker restart mysql

测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:

show master status;

 2.安装Canal

2.1创建网络

我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:

docker network create heima

让mysql加入这个网络:

docker network connect heima mysql

2.2安装Canal

提前准备好的canal的镜像压缩包 canal.tar

docker load -i canal.tar

2.3运行Canal容器

然后运行命令创建Canal容器:

docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \
-e canal.instance.master.address=mysql:3306  \
-e canal.instance.dbUsername=canal  \
-e canal.instance.dbPassword=canal  \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false  \
-e canal.instance.filter.regex=heima\\..* \
--network heima \
-d canal/canal-server:v1.1.5

说明:

  • -p 11111:11111:这是canal的默认监听端口

  • -e canal.instance.master.address=mysql:3306:数据库地址和端口,如果不知道mysql容器地址,可以通过docker inspect 容器id来查看

  • -e canal.instance.dbUsername=canal:数据库用户名

  • -e canal.instance.dbPassword=canal :数据库密码

  • -e canal.instance.filter.regex=:要监听的表名称

 表名称监听支持的语法:

mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\) 
常见例子:
1.  所有表:.*   or  .*\\..*
2.  canal schema下所有表: canal\\..*
3.  canal下的以canal打头的表:canal\\.canal.*
4.  canal schema下的一张表:canal.test1
5.  多个规则组合使用然后以逗号隔开:canal\\..*,mysql.test1,mysql.test2 

2.4查看canal运行状态

1.Canal容器启动日志

docker logs -f canal

 2.查看canal是否和mysql建立连接

进入canal容器

docker exec -it canal bash

查看canal日志

tail -f canal-server/logs/canal/canal.log

 查看canal和数据库相关的日志

tail -f canal-server/logs/heima/heima.log

 Canal客户端应用

 

 要和启动canal时配置的desitinations一致

 

 @Id,@Transient,@Column 使用spring的注解就可以

        <dependency>
            <groupId>top.javatool</groupId>
            <artifactId>canal-spring-boot-starter</artifactId>
            <version>1.2.1-RELEASE</version>
        </dependency>


canal:
  destination: heima
  server: 192.168.150.101:11111

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;

import javax.persistence.Column;
import java.util.Date;
@Data
@TableName("tb_item")
public class Item {
    @TableId(type = IdType.AUTO)
    @Id
    private Long id;//商品id
    @Column(name = "name")
    private String name;//商品名称
    private String title;//商品标题
    private Long price;//价格(分)
    private String image;//商品图片
    private String category;//分类名称
    private String brand;//品牌名称
    private String spec;//规格
    private Integer status;//商品状态 1-正常,2-下架
    private Date createTime;//创建时间
    private Date updateTime;//更新时间
    @TableField(exist = false)
    @Transient
    private Integer stock;
    @TableField(exist = false)
    @Transient
    private Integer sold;
}

@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {

    @Autowired
    private RedisHandler redisHandler;
    @Autowired
    private Cache<Long, Item> itemCache;

    @Override
    public void insert(Item item) {
        // 写数据到JVM进程缓存
        itemCache.put(item.getId(), item);
        // 写数据到redis
        redisHandler.saveItem(item);
    }

    @Override
    public void update(Item before, Item after) {
        // 写数据到JVM进程缓存
        itemCache.put(after.getId(), after);
        // 写数据到redis
        redisHandler.saveItem(after);
    }

    @Override
    public void delete(Item item) {
        // 删除数据到JVM进程缓存
        itemCache.invalidate(item.getId());
        // 删除数据到redis
        redisHandler.deleteItemById(item.getId());
    }
}

总结

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
高并发情况下,为了优化资源请求流程,可以采取以下措施: 1. 缓存:使用缓存来减轻数据库和其他资源的负载压力。将经常请求的数据缓存在内存中,避免频繁地访问后端资源。 2. 异步处理:将一些耗时的操作异步化,例如通过消息队列将请求放入队列中进行处理,减少请求的响应时间。这样可以将请求的处理和资源的分配解耦,提高系统的并发能力。 3. 分布式架构:采用分布式架构来横向扩展系统的处理能力。将系统拆分为多个服务或者微服务,每个服务负责处理一部分请求,通过负载均衡将请求分发到不同的服务节点上。 4. 数据库优化:对数据库进行性能优化,包括合理设计数据库表结构、建立索引、优化SQL查询语句等。避免全表扫描和大量的IO操作,提高数据库的读写性能。 5. 队列和限流:使用消息队列来平滑处理请求峰值,避免资源过载。同时,可以设置请求的限流策略,控制系统的最大并发数,保证系统稳定运行。 6. 分级缓存:采用多级缓存策略,将缓存分为多个层级,根据不同的数据访问频率和重要性进行缓存策略的选择。例如,将热点数据缓存在内存中,将较少访问的数据缓存在磁盘上。 7. 水平扩展:通过增加服务器数量来提高系统的处理能力。可以采用负载均衡技术将请求均匀地分发到多个服务器上,实现水平扩展。 以上是一些常见的优化流程措施,具体的优化方案需要根据系统的具体情况进行调整和选择。同时,对于高并发场景,还需要进行系统监控和性能测试,及时发现和解决潜在的问题,保证系统的稳定性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值