传统缓存策略
一般是请求到达Tomcat,先查询Redis,如果未命中则查询数据库。
存在问题:
· 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
· Redis缓存失效时,会对数据库产生冲击
多级缓存方案
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能
用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理:
缓存分两类:
· 分布式缓存,如Redis:
· 优点:存储容量更大、可靠性更好、可以在集群间共享
· 缺点:访问缓存有网络开销
· 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
· 进程本地缓存,如HashMap、GuavaCache:
· 优点:读取本地内存,没有网络开销,速度更快
· 缺点:存储容量有限、可靠性较低、无法共享
· 场景:性能要求较高,缓存数据量较小
Caffeine是基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库(Spring内部的缓存使用的就是Caffeine)
@Test
void testBasicOps() {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("gf", "迪丽热巴");
// 取数据,不存在则返回null
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 取数据,不存在则去数据库查询
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}
Caffeine提供三种缓存驱逐策略:
· 基于容量:设置缓存的数量上限
· 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差
默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
1、创建bean,设置本地缓存Cache初始大小和上限:
@Configuration
public class CaffeineConfig {
@Bean
public Cache<Long, Item> itemCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10000)
.build();
}
@Bean
public Cache<Long, ItemStock> stockCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10000)
.build();
}
}
2、在Controller层调用本地缓存get方法:
@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id){
return itemCache.get(id, key -> itemService.query()
.ne("status", 3).eq("id",key)
.one()
);
}
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id){
return stockCache.get(id,key -> stockService.getById(key));
}
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能
数据类型
数据类型 | 描述 |
---|---|
nil | 只有值nil属于该类,表示一个无效值(在条件表达式中相当于false) |
boolean | 包含两个值:false和true |
number | 表示双精度类型的实浮点数 |
string | 字符串由一对双引号或单引号来表示,采用 . . 进行拼接 |
function | 由 C 或 Lua 编写的函数 |
table | Lua中的表(table)其实是一个“关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。在Lua里,table的创建是通过“构造表达式”来完成,最简单构造表达式是{ },用来创建一个空表 |
变量
Lua声明变量时,不需要指定数据类型:
访问table:
循环
数组、table都可利用for循环遍历:
· 遍历数组:
条件控制、函数
函数
定义函数的语法:
条件控制
类似Java的条件控制,例如if、else语法:
OpenResty是一个基于Nginx的高性能Web平突然,用于方便搭建能够处理超高并发、扩展性极高的动态Web应用、Web服务和动态网关。
特点:
· 具备Nginx的完整功能
· 基于Lua语言进行扩展,集成了大量精良的Lua库、第三方模块
· 允许使用Lua自定义业务逻辑、自定义库
Linux虚拟机必须联网
首先要安装OpenResty的依赖开发库,执行命令:
yum install -y pcre-devel openssl-devel gcc --skip-broken
你可以在你的 CentOS 系统中添加 openresty
仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum check-update
命令)。运行下面的命令就可以添加我们的仓库:
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
如果提示说命令不存在,则运行:
yum install -y yum-utils
然后再重复上面的命令
然后就可以像下面这样安装软件包,比如 openresty
:
yum install -y openresty
opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。
如果你想安装命令行工具 opm
,那么可以像下面这样安装 openresty-opm
包:
yum install -y openresty-opm
默认情况下,OpenResty安装的目录是:/usr/local/openresty
OpenResty就是在Nginx基础上集成了一些Lua模块。
打开配置文件:
vi /etc/profile
在最下面加入两行:
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH
NGINX_HOME:后面是OpenResty安装目录下的nginx的目录
然后让配置生效:
source /etc/profile
运行方式
# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop
在Linux的控制台输入命令以启动nginx:
nginx
然后访问页面:http:/ /自己虚拟机地址 :8081
在nginx.conf的http下面,添加 加载OpenResty的lua模块:
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
在nginx.conf的server下,添加 /api/item路径的监听:
location /api/item {
# 响应类型,返回json
default_type application/json;
# 响应数据由 lua/item.lua文件决定
content_by_lua_file lua/item.lua;
}
nginx提供了内部API用以发送http请求:
返回的响应包括:
· resp.status:响应状态码
· resp.header:响应头(是一个table)
· resp.body:响应体(即响应数据)
注意
这里的path路径不包含IP、端口。该请求会被nginx内部的server监听并处理。需要编写一个server对该路径做反向代理:
封装http查询函数
1、在/usr/local/openresty/lualib目录下创建common.lua文件:
vi /usr/local/openresty/lualib/common.lua
2、在common.lua中封装http查询函数
-- 封装函数,发送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
导入封装的函数
在item.lua中进行修改:
--导入common函数库
local common = require('common')
--获取common.lua中的封装函数
local read_http = common.read_http
JSON结果处理
cjson模块用来处理JSON的序列化和反序列化
· 引入cjson模块:
local cjson = require "cjson"
· 序列化:
local obj = {
name = 'jack',
age = 21
}
local json = cjson.encode(obj)
· 反序列化:
local json = '{"name":"jack","age":21}'
--反序列化
local obj = cjson.decode(json);
print(obj.name)
冷启动
服务刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力
缓存预热
可利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中
1、Docker运行Redis容器
docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
2、在item-service服务中引入Redis依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
3、配置Redis地址
spring:
redis:
host: 192.168.159.128 # 换成自己虚拟机地址
4、编写初始化类RedisHandler
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
//Spring里默认的JSON处理工具
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
//初始化缓存
// 1查询商品信息
List<Item> itemList = itemService.list();
// 2存入缓存
for (Item item : itemList) {
// 2.1 item序列化为JSON
String s = MAPPER.writeValueAsString(item);
// 2.2 存入redis
redisTemplate.opsForValue().set("item:id:"+item.getId(),s);
}
// 3查询库存信息
List<ItemStock> stockList = stockService.list();
// 2存入缓存
for (ItemStock stock : stockList) {
// 2.1 item序列化为JSON
String json = MAPPER.writeValueAsString(stock);
// 2.2 存入redis
redisTemplate.opsForValue().set("item:stock:id:"+stock.getId(),json);
}
}
}
查询Redis缓存
OpenResty提供了操作Redis的模块,在common.lua中引入:
· 引入Redis模块,并初始化Redis对象
· 封装函数,用来释放Redis连接(放入连接池)
· 封装函数,从Redis读数据并返回
· 将封装好的函数 暴露出去:
· 最后在item.lua文件中引用,保存并运行nginx -s reload
重新加载nginx:
OpenResty为Nginx提供了shard dict的功能,可在nginx的多个worker之间共享数据,实现缓存功能
· 开启共享字典,在nginx.conf的http下添加配置:
通过/usr/local/openresty/nginx/logs下的error.log日志来查看错误
先cd /usr/local/openresty/nginx
,再tail -f logs/error.log
常见方式:
· 设置有效期:
给缓存设置有效期,到期后自动删除。再次查询时更新
· 优势:简单、方便
· 缺点:时效性差,缓存过期之前可能不一致
· 场景:更新频率较低,时效性要求低的业务
· 同步双写:
在修改数据库的同时,直接修改缓存
· 优势:时效性强,缓存与数据库强一致
· 缺点:有代码侵入,耦合度高
· 场景:对一致性、时效性要求较高的缓存数据
· 异步通知:
修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
· 优势:低耦合,可同时通知多个缓存服务
· 缺点:时效性一般,可能存在中间不一致状态
· 场景:时效性要求一般,有多个服务需要同步
canal是阿里巴巴旗下一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。
Canal是基于mysql的主从同步实现的。
MySQL主从同步的原理:
· MySQL master 将数据变更写入二进制日志(binary log),其中记录的数据叫binary log events
· MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)
· MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
Canal把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal客户端,进而完成对其它数据库同步。
Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。
这里以之前用Docker运行的mysql为例:
打开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-binbinlog-do-db=heima
:指定对哪个database记录binary log events,这里记录heima这个库最终效果:
[mysqld]
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
接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。(在Navicat的heima数据库中新建查询)
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;
当 从节点的position与主 不一致时,需要获取新的log
我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:
docker network create heima
让mysql加入这个网络:
docker network connect heima mysql
拉取镜像,然后运行命令创建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
通过docker logs -f canal
命令可查看是否开启成功
通过docker exec -it canal bash
进入容器,tail -f canal-server/logs/canal/canal.log
查看日志
Canal提供各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端
使用第三方开源的canal-starter
引入依赖:
<dependency>
<groupId>top.javatoolgroupId>
<artifactId>canal-spring-boot-starterartifactId>
<version>1.2.1-RELEASEversion>
dependency>
编写配置:
canal:
destination: heima # canal实例名称,要跟canal-server运行时设置的destination一致
server: 192.168.159.128:11111 # canal地址
编写监听器,监听Canal消息:
@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) {
// 写数据到redis
redisHandler.saveItem(item);
//写数据到JVM缓存
itemCache.put(item.getId(),item);
}
@Override
public void update(Item before, Item after) {
// 写数据到redis
redisHandler.saveItem(after);
//写数据到JVM缓存
itemCache.put(after.getId(),after);
}
@Override
public void delete(Item item) {
// 删除redis数据
redisHandler.deleteById(item.getId());
// 删除JVM缓存数据
itemCache.invalidate(item.getId());
}
}
Canal客户端
Canal推送给canal-client的是被修改的这一行数据(row)。 而我们引入的canal-client会帮我们把行数据封装到Item实体类中。