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());
}
}
总结