• Spring Cloud之多级缓存


    目录

    传统缓存

    多级缓存

    JVM进程缓存

    Caffeine

    缓存驱逐策略

    实现进程缓存

    常用Lua语法

    数据类型

    变量声明

    循环使用

    定义函数

    条件控制

    安装OpenResty

    实现Nginx业务逻辑编写

    请求参数解析

    实现lua访问tomcat

    JSON的序列化和反序列化

    Tomcat的集群负载均衡

    添加Redis缓存

    启动Redis

    查询Redis缓存

    Nginx本地缓存

    缓存同步策略

    Canal

    安装和配置Canal

    监听Canal

    多级缓存访问流程


    资料下载:day04-多级缓存

    下载完成后跟着案例导入说明去做

    传统缓存

    传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,存在下面的问题

    • 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
    • Redis缓存失效时,会对数据库产生冲击

    多级缓存

    多级缓存主要压力在于nginx,在生产环境中,我们需要通过部署nginx本地缓存集群以及一个nginx反向代理到本地缓存

    JVM进程缓存

    缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:

    • 分布式缓存,例如Redis:
      • 优点:存储容量更大、可靠性更好、可以在集群间共享
      • 缺点:访问缓存有网络开销
      • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
    • 进程本地缓存,例如HashMap、GuavaCache
      • 优点:读取本地内存,没有网络开销,速度更快
      • 缺点:存储容量有限、可靠性较低、无法共享
      • 场景:性能要求较高,缓存数据量较小

    Caffeine

    案例测试代码

    1. @Test
    2. void testBasicOps() {
    3. // 创建缓存对象
    4. Cache cache = Caffeine.newBuilder().build();
    5. // 存数据
    6. cache.put("name", "张三");
    7. // 取数据,不存在则返回null
    8. String name = cache.getIfPresent("name");
    9. System.out.println("name = " + name);
    10. // 取数据,不存在则去数据库查询
    11. String defaultName = cache.get("defaultName", key -> {
    12. // 这里可以去数据库根据 key查询value
    13. return "李四";
    14. });
    15. System.out.println("defaultName = " + defaultName);
    16. }

    运行结果如下

    缓存驱逐策略

    Caffeine提供了三种缓存驱逐策略:

    • 基于容量:设置缓存的数量上限
    • 基于时间:设置缓存的有效时间
    • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据,性能较差。

    默认情况下,当缓存数据过期时,并不会立即将其清理和驱逐,而是在一次读或写操作后,或是在空闲时间完成对失效数据的驱逐。

    基于容量实现

    1. /*
    2. 基于大小设置驱逐策略:
    3. */
    4. @Test
    5. void testEvictByNum() throws InterruptedException {
    6. // 创建缓存对象
    7. Cache cache = Caffeine.newBuilder()
    8. // 设置缓存大小上限为 1
    9. .maximumSize(1)
    10. .build();
    11. // 存数据
    12. cache.put("name1", "张三");
    13. cache.put("name2", "李四");
    14. cache.put("name3", "王五");
    15. // 延迟10ms,给清理线程一点时间
    16. Thread.sleep(10L);
    17. // 获取数据
    18. System.out.println("name1: " + cache.getIfPresent("name1"));
    19. System.out.println("name2: " + cache.getIfPresent("name2"));
    20. System.out.println("name3: " + cache.getIfPresent("name3"));
    21. }

    运行结果如下 

    基于时间实现

    1. /*
    2. 基于时间设置驱逐策略:
    3. */
    4. @Test
    5. void testEvictByTime() throws InterruptedException {
    6. // 创建缓存对象
    7. Cache cache = Caffeine.newBuilder()
    8. .expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒
    9. .build();
    10. // 存数据
    11. cache.put("name", "张三");
    12. // 获取数据
    13. System.out.println("name: " + cache.getIfPresent("name"));
    14. // 休眠一会儿
    15. Thread.sleep(1200L);
    16. System.out.println("name: " + cache.getIfPresent("name"));
    17. }

    运行结果如下 

    实现进程缓存

    利用Caffeine实现下列需求:

    • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
    • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
    • 缓存初始大小为100
    • 缓存上限为10000

    添加缓存对象

    1. @Configuration
    2. public class CaffeineConfig {
    3. /**
    4. * 商品信息缓存
    5. * @return
    6. */
    7. @Bean
    8. public Cache itemCache(){
    9. return Caffeine.newBuilder()
    10. .initialCapacity(100)
    11. .maximumSize(10_000)
    12. .build();
    13. }
    14. /**
    15. * 商品库存缓存
    16. * @return
    17. */
    18. @Bean
    19. public Cache itemStockCache(){
    20. return Caffeine.newBuilder()
    21. .initialCapacity(100)
    22. .maximumSize(10_000)
    23. .build();
    24. }
    25. }

    在ItemController中写入查询本地缓存的方法

    1. @Autowired
    2. private Cache itemCache;
    3. @Autowired
    4. private Cache itemStockCache;
    5. @GetMapping("/{id}")
    6. public Item findById(@PathVariable("id") Long id) {
    7. return itemCache.get(id, key -> {
    8. return itemService.query()
    9. .ne("status", 3).eq("id", key)
    10. .one();
    11. }
    12. );
    13. }
    14. @GetMapping("/stock/{id}")
    15. public ItemStock findStockById(@PathVariable("id") Long id) {
    16. return itemStockCache.get(id,key->{
    17. return stockService.getById(id);
    18. });
    19. }

    修改完成后,访问localhost:8081/item/10001,观察控制台

    存在一次数据库查询。后续再次查询相同id数据不会再次查询数据库。至此实现了JVM进程缓存。

    常用Lua语法

    Nginx与Redis的业务逻辑编写并不是通过Java语言,而是通过Lua。Lua是一种轻量小巧的脚本语言,用标准的C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

    入门案例,输出hello world

    在linux中创建一个文本文件

    1. touch hello.lua
    2. # 进入vi模式
    3. vi hello.lua
    4. # 打印hello world。输入以下内容
    5. print("hello world")
    6. # 保存退出后,运行lua脚本
    7. lua hello.lua

    或是直接输入命令启动lua控制台

    lua

    直接输入命令即可

    数据类型

    数据类型

    描述

    nil

    表示一个无效值,类似于Java中的null,但在条件表达式中代表false

    boolean

    包含:true与false

    number

    表示双精度类型的实浮点数(简单来说,是数字都可以使用number表示)

    string

    字符串,由单引号或双引号来表示

    function

    由C或是Lua编写的函数

    table

    Lua中的表其实是一个“关联数组”,数组的索引可以是数字,字符串或表类型。在 Lua里,table的创建是通过“构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。

    变量声明

    Lua声明变量的时候,并不需要指定数据类型

    1. -- local代表局部变量,不加修饰词,代表全局变量
    2. local str ='hello'
    3. local num =10
    4. local flag =true
    5. local arr ={'java','python'} --需要注意的是,访问数组元素时,下标是从1开始
    6. local table ={name='Jack',age=10} --类似于Java中的map类型,访问数据时是通过table['key']或是table.key

    循环使用

    1. -- 声明数组
    2. local arr={'zhangsan','lisi','wangwu'}
    3. -- 进行循环操作
    4. for index,value in ipairs(arr) do
    5. print(index,value)
    6. end
    7. -- lua 脚本中,for循环从do开始end结束,数组解析使用ipairs
    8. -- 声明table
    9. local table={name='zhangsan',age=10}
    10. -- 进行循环操作
    11. for key,value in pairs(table) do
    12. print(key,value)
    13. end
    14. -- table解析使用pairs

    执行lua脚本

    定义函数

    1. -- 声明数组
    2. local arr={'zhangsan','lisi','wangwu'}
    3. -- 定义函数
    4. local function printArr(arr)
    5. for index,value in ipairs(arr) do
    6. print(index,value)
    7. end
    8. end
    9. -- 执行函数
    10. printArr(arr)

    执行lua脚本

    条件控制

    操作符

    描述

    实例

    and

    逻辑与操作符。若A为false,则返回A,否则返回B

    (A and B)为false

    or

    逻辑或操作符。若A为true,则返回A,否则返回B

    (A or B)为true

    not

    逻辑非操作符。与逻辑运算结果相反

    not(A and B)为true

    1. -- 声明数组
    2. local table={name='zhangsan',sex='boy',age=15}
    3. -- 定义函数
    4. local function printTable(arr)
    5. if(not arr) then
    6. print('table中不存在该字段')
    7. return nil
    8. end
    9. print(arr)
    10. end
    11. -- 执行函数
    12. printTable(table.name)
    13. printTable(table.addr)

    执行lua脚本

    安装OpenResty

    是基于Nginx的一个组件,主要作用是对Nginx编写业务逻辑

    1. yum install -y pcre-devel openssl-devel gcc --skip-broken
    2. yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
    3. # 如果失败则先执行下面一条语句后再执行上面这条
    4. yum install -y yum-utils
    5. yum install -y openresty
    6. yum install -y openresty-opm

    配置nginx的环境变量

    1. vi /etc/profile
    2. # 在最下面插入如下信息
    3. export NGINX_HOME=/usr/local/openresty/nginx
    4. export PATH=${NGINX_HOME}/sbin:$PATH
    5. # 保存后刷新配置
    6. source /etc/profile

    修改/usr/local/openresty/nginx/conf/nginx.conf配置文件如下

    1. #user nobody;
    2. worker_processes 1;
    3. error_log logs/error.log;
    4. events {
    5. worker_connections 1024;
    6. }
    7. http {
    8. include mime.types;
    9. default_type application/octet-stream;
    10. sendfile on;
    11. keepalive_timeout 65;
    12. server {
    13. listen 8081;
    14. server_name localhost;
    15. location / {
    16. root html;
    17. index index.html index.htm;
    18. }
    19. error_page 500 502 503 504 /50x.html;
    20. location = /50x.html {
    21. root html;
    22. }
    23. }
    24. }

    启动nginx

    # 启动nginx

    nginx

    # 重新加载配置

    nginx -s reload

    # 停止

    nginx -s stop

    启动后,访问虚拟机的8081端口,如果正常跳转页面如下

    实现Nginx业务逻辑编写

    先分析请求转发流程。打开win系统上的nginx路由配置文件

    接下来就需要对虚拟机中的nginx添加业务逻辑了

    对虚拟机Nginx中的配置文件添加如下代码

    1. # 放入http模块下
    2. #lua 模块
    3. lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    4. #c模块
    5. lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
    6. # 放入server模块下
    7. location /api/item {
    8. # 响应类型为json
    9. default_type application/json;
    10. # 响应结果来源
    11. content_by_lua_file lua/item.lua;
    12. }

    编写lua脚本

    在nginx目录下创建lua文件夹,并创建lua脚本

    1. mkdir lua
    2. touch lua/item.lua

    先使用假数据测试是否可以正常响应

    ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')

    访问localhost/item.html?id=10001。查看控制台是否正常响应。如果出现如下错误,去观察win系统下的nginx日志,我的打印了如下错误

    2023/11/07 19:29:38 [error] 16784#2812: *34 connect() failed (10061: No connection could be made because the target machine actively refused it) while connecting to upstream, client: 127.0.0.1, server: localhost, request: "GET /api/item/10001 HTTP/1.1", upstream: "http://192.168.10.10:8081/api/item/10001", host: "localhost", referrer: "http://localhost/item.html?id=10001"

    解决方法,打开任务管理器,将所有关于nginx的服务全部结束再次重启win系统下的nginx即可。如果不是此类错误,请查看linux系统下的错误日志。

    请求参数解析

    参数格式

    参数实例

    参数解析代码示例

    路径占位符

    /item/1001

    拦截路径中:location ~ /item/(\d+){}

    ~:表示使用正则表达式

    (\d+):表示至少有一位数字

    Lua脚本中:local id = ngx.var[1]

    匹配到的参数会存入ngx.var数组中,通过下标获取

    请求头

    id:1001

    获取请求头,返回值是table类型

    local headers = ngx.req.get_headers()

    Get请求参数

    ?id=1001

    获取GET请求参数,返回值是table类型

    local getParams = ngx.req.get_uri_args()

    Post表单参数

    id=1001

    读取请求体:ngx.req.read_body()

    获取POST表单参数,返回值是table类型

    local postParams = ngx.req.get_post_args()

    JSON参数

    {"id": 1001}

    读取请求体:ngx.reg.read bodv()

    获取body中的ison参数,返回值是string类型

    local jsonBody = ngx.req.get_body_data()

    修改linux中nginx的配置文件,实现参数解析

    1. location ~ /api/item/(\d+) {
    2. # 响应类型为json
    3. default_type application/json;
    4. # 响应结果来源
    5. content_by_lua_file lua/item.lua;
    6. }

    修改lua脚本

    1. -- 获取参数
    2. local id = ngx.var[1]
    3. -- 返回结果
    4. ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')

    访问id为10002的参数,可以发现id随着参数改变,而不是伪数据了

    实现lua访问tomcat

    nginx提供了内部API用来发送http请求

    1. local resp = ngx.location.capture("/path",{
    2. method = ngx.HTTP_GET,-- 请求方式
    3. args = {a=1,b=2},-- get方式传参数
    4. body ="c=3&d=4" -- post方式传参数
    5. })

    返回响应结果内容包括:

    • resp.status:响应状态码
    • resp.header:响应头,是一个table
    • resp.body:响应体,就是响应数据

    需要注意的是,/path不会指定IP地址和端口而是会被内部拦截,这个时候我们还需要编写一个路由器,发送到对应的服务器。修改linux中的nginx.conf文件添加如下配置

    1. location /item {
    2. proxy_pass http://192.168.10.11:8081;
    3. }

    发起Http请求我们可以封装成一个方法,让其他请求发起时也可以调用,因此,我们可以在lualib文件夹下,创建lua脚本。

    1. -- 封装函数,发送http请求,并解析响应
    2. local function read_http(path, params)
    3. local resp = ngx.location.capture(path,{
    4. method = ngx.HTTP_GET,
    5. args = params,
    6. })
    7. if not resp then
    8. -- 记录错误信息,返回404
    9. ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
    10. ngx.exit(404)
    11. end
    12. return resp.body
    13. end
    14. -- 将方法导出
    15. local _M = {
    16. read_http = read_http
    17. }
    18. return _M

    修改item.lua脚本,不再返回伪数据,而是查询真实的数据

    1. -- 导入common函数库
    2. local common = require('common')
    3. local read_http = common.read_http
    4. -- 获取参数
    5. local id = ngx.var[1]
    6. -- 查询商品信息
    7. local itemJSON = read_http('/item/'..id,nil)
    8. -- 查询库存信息
    9. local stockJSON = read_http('/item/stock/'..id,nil)
    10. -- 返回结果
    11. ngx.say(itemJSON)

    这里只返回了商品信息,接下来访问其他id的商品,查看是否可以查询出商品信息

    JSON的序列化和反序列化

    引入cjson模块,实现序列化与反序列化

    1. -- 导入common函数库
    2. local common = require('common')
    3. local cjson = require('cjson')
    4. local read_http = common.read_http
    5. -- 获取参数
    6. local id = ngx.var[1]
    7. -- 查询商品信息
    8. local itemJSON = read_http('/item/'..id,nil)
    9. -- 查询库存信息
    10. local stockJSON = read_http('/item/stock/'..id,nil)
    11. -- 反序列化JSON商品信息为table类型数据
    12. local item = cjson.decode(itemJSON)
    13. local stock = cjson.decode(stockJSON)
    14. -- 数据组合
    15. item.stock = stock.stock
    16. item.sold = stock.sold
    17. -- 序列化为JSON
    18. -- 返回结果
    19. ngx.say(cjson.encode(item))

    Tomcat的集群负载均衡

    这里我们访问的服务端口是写死的,但通常tomcat是一个集群,因此,我们需要修改我们linux的配置文件,配置tomcat集群

    由于Tomcat的负载均衡策略为轮询,那么就会产生一个问题,tomcat集群的进程缓存是不共享的,也就是说,第一次访问8081生成的缓存,在第二次访问8082时,是不存在的,会在8082也生成一份相同的缓存。所以我们需要保证访问同一个id的请求,会被路由到存在缓存的那个tomcat服务器上。这就需要我们修改负载均衡算法。实际实现很简单,只需要在tomcat集群配置添加一行

    实现原理是,nginx会对拦截到的请求进行hash算法,然后对集群数量进行取余。从而保证对同一个id的请求都会被路由到同一个tomcat服务器。

    添加Redis缓存

    本地缓存在访问进程缓存之间,应该先去查询Redis缓存,在添加Redis缓存时,又存在冷启动与缓存预热问题。

    • 冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。
    • 缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保

    启动Redis

    在docker中输入如下命令

    docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes

    启动成功后使用RESP连接redis

    成功连接后,我们需要进行预热,我们的数据不多,将所有的数据全都缓存进去即可,编写一个初始化Handler

    1. @Component
    2. public class RedisHandler implements InitializingBean {
    3. @Autowired
    4. private StringRedisTemplate redisTemplate;
    5. @Autowired
    6. private ItemService itemService;
    7. @Autowired
    8. private ItemStockService itemStockService;
    9. private final static ObjectMapper MAPPER = new ObjectMapper();
    10. /**
    11. * Bean生命周期之生成Bean对象之后属性填充
    12. *
    13. * @throws Exception
    14. */
    15. @Override
    16. public void afterPropertiesSet() throws Exception {
    17. //将数据库中的数据进行填充
    18. //查询商品数据并填充
    19. List listItems = itemService.list();
    20. List listStock = itemStockService.list();
    21. for (Item listItem : listItems) {
    22. String itemJson = MAPPER.writeValueAsString(listItem);
    23. redisTemplate.opsForValue().set("itemInfo:id:"+listItem.getId(),itemJson);
    24. }
    25. for (ItemStock itemStock : listStock) {
    26. String itemJson = MAPPER.writeValueAsString(itemStock);
    27. redisTemplate.opsForValue().set("itemStock:id:"+itemStock.getId(),itemJson);
    28. }
    29. }
    30. }

    重启项目,我们就可以看到redis中已经存在了商品数据

    查询Redis缓存

    启动成功并添加数据后,我们接下来去实现本地缓存查询Redis缓存。这个时候我们还需要编写lua脚本

    修改common.lua脚本

    1. -- 引入redis的函数库
    2. local redis = require('resty.redis')
    3. -- 初始化redis对象
    4. local red = redis:new()
    5. red:set_timeouts(1000,1000,1000)
    6. -- 关闭redis连接的工具方法,其实是放入连接池
    7. local function close_redis(red)
    8. local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
    9. local pool_size = 100 --连接池大小
    10. local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    11. if not ok then
    12. ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
    13. end
    14. end
    15. -- 查询redis的方法 ip和port是redis地址,key是查询的key
    16. local function read_redis(ip, port, key)
    17. -- 获取一个连接
    18. local ok, err = red:connect(ip, port)
    19. if not ok then
    20. ngx.log(ngx.ERR, "连接redis失败 : ", err)
    21. return nil
    22. end
    23. -- 查询redis
    24. local resp, err = red:get(key)
    25. -- 查询失败处理
    26. if not resp then
    27. ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
    28. end
    29. --得到的数据为空处理
    30. if resp == ngx.null then
    31. resp = nil
    32. ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
    33. end
    34. close_redis(red)
    35. return resp
    36. end
    37. local _M = {
    38. read_http = read_http,
    39. read_redis = read_redis
    40. }

    修改item.lua脚本

    1. -- 导入common函数库
    2. local common = require('common')
    3. local cjson = require('cjson')
    4. local read_http = common.read_http
    5. local read_redis = common.read_redis
    6. -- 封装函数
    7. function read_data(key,path,params)
    8. --查询Redis
    9. local resp = read_redis('127.0.0.1',6379,key)
    10. if not resp then
    11. ngx.log("查询redis失败,key为:",key)
    12. resp = read_http(path,params)
    13. end
    14. return resp
    15. end
    16. -- 获取参数
    17. local id = ngx.var[1]
    18. -- 查询商品信息
    19. local itemJSON = read_data('itemInfo:id:'..id,'/item/'..id,nil)
    20. -- 查询库存信息
    21. local stockJSON = read_data('itemStock:id:'..id,'/item/stock/'..id,nil)
    22. -- 反序列化JSON商品信息为table类型数据
    23. local item = cjson.decode(itemJSON)
    24. local stock = cjson.decode(stockJSON)
    25. -- 数据组合
    26. item.stock = stock.stock
    27. item.sold = stock.sold
    28. -- 序列化为JSON
    29. -- 返回结果
    30. ngx.say(cjson.encode(item))

    我们关闭tomcat服务,直接访问,测试是否是通过Redis获取到内容

    Nginx本地缓存

    接下来我们去实现在本地缓存中进行查询

    OpenResty为Nginx提供了shard dict的功能,可以在nginx的多的worker之间共享数据,实现缓存功能。

    修改CentOS中的nginx.conf文件,开启该功能。

    1. #开启共享字典,名字叫item_cache,缓存大小150兆
    2. lua_shared_dict item_cache 150m;

    接下来修改item.lua中的read_data代码,先进行本地查询

    1. -- 导入common函数库
    2. local common = require("common")
    3. local cjson = require('cjson')
    4. local read_http = common.read_http
    5. local read_redis = common.read_redis
    6. -- 获取本地缓存对象
    7. local item_cache = ngx.shared.item_cache
    8. -- 封装函数
    9. function read_data(key,expire,path,params)
    10. --先去查询本地缓存
    11. local val = item_cache:get(key)
    12. if not val then
    13. --查询Redis
    14. ngx.log(ngx.ERR,"本地缓存不存在,去查询redis")
    15. val = read_redis("127.0.0.1",6379,key)
    16. if not val then
    17. ngx.log(ngx.ERR,"查询redis失败,key为:",key)
    18. val = read_http(path,params)
    19. end
    20. end
    21. -- 将数据写入本地缓存,并设置过期时间
    22. item_cache:set(key,val,expire)
    23. return val
    24. end
    25. -- 获取参数
    26. local id = ngx.var[1]
    27. -- 查询商品信息
    28. local itemJSON = read_data("itemInfo:id:"..id,1800,'/item/'..id,nil)
    29. ngx.log(ngx.ERR,"itmeJson的信息为:",itemJSON)
    30. -- 查询库存信息
    31. local stockJSON = read_data("itemStock:id:"..id,60,'/item/stock/'..id,nil)
    32. -- 反序列化JSON商品信息为table类型数据
    33. local item = cjson.decode(itemJSON)
    34. local stock = cjson.decode(stockJSON)
    35. -- 数据组合
    36. item.stock = stock.stock
    37. item.sold = stock.sold
    38. -- 序列化为JSON
    39. -- 返回结果
    40. ngx.say(cjson.encode(item))

    接下来进行页面访问。第一次访问结果如下,后续再次访问不会再打印日志。说明的确是走了本地缓存

    缓存同步策略

    缓存数据同步的常见方式有三种:

    设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新

    • 优势:简单、方便
    • 缺点:时效性差,缓存过期之前可能不一致
    • 场景:更新频率较低,时效性要求低的业务

    同步双写:在修改数据库的同时,直接修改缓存

    • 优势:时效性强,缓存与数据库强一致
    • 缺点:有代码侵入,耦合度高;
    • 场景:对一致性、时效性要求较高的缓存数据

    异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

    • 优势:低耦合,可以同时通知多个缓存服务
    • 缺点:时效性一般,可能存在中间不一致状态
    • 场景:时效性要求一般,有多个服务需要同步

    Canal

    Canal,译意为水道/管道/沟渠,Canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。

    Canal是基于MySQL的主从同步来实现的,MySQL主从同步的原理如下:

    MySQL master将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events

    MySQL slave将master的binary log events拷贝到它的中继日志(relay log)

    MySQL slave重放relay log中事件,将数据变更反映它自己的数据。

    Cansl将自己伪装成MySQL的一个节点,从而监听master的binary log变化。再将得到的变化信息传递到Canal的客户端,从而完成对其他数据库的同步。

    安装和配置Canal

    首先要进行文件配置。开启binlog功能

    1. # 进入MySQL的配置文件
    2. vi /tmp/mysql/conf/my.cnf
    3. # 添加如下内容
    4. # binary log存放位置
    5. log-bin=/var/lib/mysql/mysql-bin
    6. # 指定数据库名称
    7. binlog-do-db=heima
    8. # 添加完成后,重启Mysql
    9. docker restart mysql

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

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

    重启mysql容器即可

    docker restart mysql

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

    show master status;

    创建网络

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

    docker network create item

    让mysql加入这个网络:

    docker network connect item mysql

    安装Canal。将资料中的Canal.tar加载到虚拟机中

    通过命令导入:

    docker load -i canal.tar

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

    1. docker run -p 11111:11111 --name canal \
    2. -e canal.destinations=item \
    3. -e canal.instance.master.address=mysql:3306 \
    4. -e canal.instance.dbUsername=canal \
    5. -e canal.instance.dbPassword=canal \
    6. -e canal.instance.connectionCharset=UTF-8 \
    7. -e canal.instance.tsdb.enable=true \
    8. -e canal.instance.gtidon=false \
    9. -e canal.instance.filter.regex=item\\..* \
    10. --network item \
    11. -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=:要监听的库名称

    监听Canal

    在项目中的pom文件中引入依赖

    1. <dependency>
    2. <groupId>top.javatoolgroupId>
    3. <artifactId>canal-spring-boot-starterartifactId>
    4. <version>1.2.1-RELEASEversion>
    5. dependency>

    添加配置文件中的内容

    1. canal:
    2. destination: item #启动时指定的容器名称
    3. server: 192.168.116.131:11111 #canal地址

    编写监听器

    1. @Component
    2. @CanalTable("tb_item")//需要监听哪个表
    3. public class ItemHandler implements EntryHandler {
    4. @Autowired
    5. private RedisHandler redisHandler;
    6. @Autowired
    7. private Cache itemCache;
    8. @Override
    9. public void insert(Item item) {
    10. //更新redis数据库
    11. redisHandler.saveItem(item);
    12. //更新JVM缓存
    13. itemCache.put(item.getId(),item);
    14. }
    15. @Override
    16. public void update(Item before, Item after) {
    17. redisHandler.saveItem(after);
    18. itemCache.put(after.getId(),after);
    19. }
    20. @Override
    21. public void delete(Item item) {
    22. redisHandler.deleteById(item.getId());
    23. itemCache.invalidate(item.getId());
    24. }
    25. }

    启动服务器,会发现控制台一直输出消息

    测试能否同步缓存修改,访问localhost:8081端口,对数据进行修改。控制台打印效果如下

    访问商品商品页面,也能够发现修改的数据发生了改变,并且服务器没有输出任何查询数据库的日志。

    多级缓存访问流程

     

  • 相关阅读:
    Django国际化与本地化指南
    kubernetesl yaml deploy rancher server
    python期末复习案例
    Rust swap
    【OpcUA开发笔记 3】Open62541证书登录方式
    UE4 碰撞射线检测
    yum小bug
    云时代下,医药行业管理居然这么简单
    Java 如何实现 List<String> 的深拷贝?
    20240301-2-ZooKeeper面试题(二)
  • 原文地址:https://blog.csdn.net/zmbwcx/article/details/134276244