Redis分布式缓存方案

分布式缓存

单节点Redis问题

  • 数据丢失:数据持久化
  • 并发能力弱:搭建主从集群,实现读写分离
  • 故障恢复问题:哨兵实现健康检测,自动恢复
  • 存储能力:搭建分片集群,利用插槽机制实现动态扩容

Redis持久化

RDB持久化
数据库备份文件,也叫快照,把内存数据存到磁盘。使用save进行主动RDB,会阻塞所有命令。建议使用bgsave开启子进程执行RDB。Redis停机时会被动执行一次RDB。

RDB:

bgsave开始会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件。
在这里插入图片描述

注:在fork时插入的数据会影响一致性,所以采用copy-on-write技术,当主进程执行读操作,刚问共享内存。当主进程执行写操作,则拷贝一份数据,执行写。

配置 :

Redis内部可在redis.conf配置RDB触发机制。

总结:
  1. RDB方式bgsave基本流程?
    -fork主进程得到子进程,共享内存空间。
    -子进程读取内存数据,并写入RDB。
    -用新RDB文件替换旧的RDB文件。
  2. RDB会在什么时候执行?save60 1000代表什么?
    -手动save或bgsave,被动服务停止时。代表60s内至少修改1000次才触发RDB。
  3. RDB缺点?
    -RDB执行间隔时间长,两次RDB之间写入数据有丢失风险。
    -fork子进程,压缩,写出RDB文件都比较耗时。
AOF:

AOF持久化
追加文件。redis每一条写命令都记录在AOF文件,是命令日志文件。

配置:

Redis的AOF默认关闭,需修改redis.conf的appendonly来开启AOF。可配置appendfsync改变刷盘策略的记录频率:always,everysec(默认),no(由操作系统决定)。

  • always 同步刷盘 可靠性高,几乎不丢失数据 性能差
  • everysec 每秒刷盘 性能中 最多丢失1s数据
  • no 操作系统控制 性能好 可靠性差,丢失数据大

auto-aof-rewrite-percentage 100 文件比上次增长超过几%则触发重写
auto-aof-rewrite-min-size 64mb 文件多大重写

问题:

AOF会比RDB大很多,而且对同一个KEY多次写操作,只有最后一次才有意义,通过执行bgrewriteaof命令,可以让AOF文件执行重写。

RDB和AOF对比

如果对数据安全性要求高,则需结合两者使用
<图:RDB和AOF对比 aof&rdb.jpg>

Redis主从集群

搭建主从架构

单节点Reds并发不高,就需要主从集群,实现读写分离。

准备

要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录。开启RDB关闭AOF。拷贝redis.conf到三个目录。

问题:为什么Redis是主从集群而不是负载均衡集群?

Redis读多写少,主节点做写操作,从节点做读操作。

开启主从关系

使用replicaof或者slaveof命令,有临时和永久两种模式。

数据同步原理

主从第一次同步是全量同步,第二次是同步记录rdb期间的所有命令,第三次发送repl_baklog中的命令。
《图:数据同步原理》

问题:master如何判断slave是不是第一次来同步数据?
  • replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集,每一次master都有唯一的replid,slave会继承master节点的replid。
  • offset:偏移量,如果slave的offset小于master的offset,说明lave的数据落后于master,需要更新。
    因此salve做数据同步,必须想master声明replicationId和offset,master才能判断传输的数据。
全量同步流程

master将完整内存数据生成rdb,发送给slave,后续命令则记录在repl_baklog,逐个发给slave
注:slave节点第一次链接master时进行全量同步,或者slave断开太久repl_baklog中的offset已被覆盖

  1. slave节点请求增量同步
  2. master节点判断replid,不一致则拒绝增量同步
  3. master将完整的内存数据生成rdb,发送到slave
  4. slave清空本地数据,加载master的rdb
  5. master将rdb期间的命令记录在repl_baklog中,并持续将log中的命令发送slave。
增量同步

slave将自己的offset提交到master,master获取repl_baklog中从offset之后的命令给slave
注:slave节点断开又恢复,并在repl_baklog中找到offset时
《图:增量同步》

优化Redis主从集群
  1. 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO
  2. Redis单节点上的内存占用不要太大,减少RDB导致的磁盘IO
  3. 适当提高repl_baklog的大小,发展slave宕机尽快回复,避免全量
  4. 限制一个master上的slave数量,太多salve建议采用主-从-从链式结构,减少master压力
问题:salve宕机可以找master同步数据,master宕机怎们办呢?

哨兵机制-主从切换

Redis哨兵

哨兵作用和原理 重要

Redis哨兵(Sentinel)机制实现主从集群的自动故障恢复。
《图:Redis哨兵》

  • 监控:Sentinel会不断检查master和slave是否按预期工作。
  • 故障恢复:master故障哨兵会将一个新的slave提升为master。当故障实例恢复后也以新的master为主。
  • 通知:Sentinel充当Redis客户端服务发现来源,当集群发生故障转移时,会将最新消息推送给Redis的客户端。

服务状态监控
Sentinel基于心跳机制检测服务状态,每1s向集群的每个实例发送ping命令:

  1. 主观下线:如果sentinel节点发现某个实例未在规定时间响应,则认为该实例主动下线。
  2. 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线,quorum值最好超过sentinel实例的一半。

选举新的master规则:

  • 首先判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds*10)则会排除该slave几点
  • 判断slave节点的slave-priority值,越小优先级越高,为0则永不参与选举。
  • 如slave-priority一样,则判断slave节点的offset值,越大说明值越新,优先级越高。
  • 最后是判断slave节点的运行id大小,越小优先级越高。

如何实现故障转移(当选择slave1为新master后):

  1. sentinel给备选的slave1发送slaveofnoone(别当奴隶了)命令,让该节点成为新的master。
  2. sentinel给其他slave发送slaveof slave1的ip+端口号,让他们成为slave1的从节点,从新的master上同步数据。
  3. sentinel将故障节点标记为slave发送slaveod命令,当故障节点恢复后会自动成为新的master的slave节点。
搭建哨兵集群

具体搭建流程参考百度:《Redis集群.md》

RedisTemplate的哨兵模式

spring的RedisTemplate底层利用lettuce实现节点的感知和自动切换。

  1. pom:spring-boot-starter-data-redis
  2. 配置sentinel信息
  3. 启动类配置主从读写分离
/**
 * MASTER:从主节点读取
 * MASTER_PREFERRED:优先master读取,不可用读取replica
 * REPLICA:从slave读取
 * ERPLICA_PREFERRED:优先从slave节点读取,所有的slave都不可用才读取master
 */
@Bean
public LettuceClientConfigurationBuilderCustomizer configurarionBuilderCustomizer(){
    return configBuilder -> configBuilder.readFrom(ReadFrom.ERPLICA_PREFERRED);
}

Redis分片集群

为什么需要分片集群?
主从和哨兵可以解决高可用,高并发读问题,但依然还有两个问题没有解决:

  • 海量数据存储问题
  • 高并发写问题

使用分片集群可以解决上述问题,分片集群特征:

  • 集群中有多个master,每个master保存不同的数据。
  • 每个master都可以有多个slave节点。
  • master之间通过ping检测彼此之间健康状态。
  • 客户端请求可以访问任何集群任意节点,最终都会被转发到正确的节点。
    《图: Redis分片集群》
搭建分片集群

具体搭建流程参考百度:《Redis分片集群.md》

散列插槽

Redis会把每一个master节点映射到0~16383个插槽(hash slot)上去,查看集群信息时就能看到。
数据的key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,计算方式为CRC16算法得到一个hash值,然后对16384取余得到slot值,分两种情况:

  • key中包含{},且{}中至少包含1个字符,{}中的部分是有效部分(key是{test}num,则根据test计算)
  • key中不包含{},整个key都是有效部分。(key是num,则根据num计算)

问题:为什么数据要绑定插槽上呢?
数据跟着插槽走,宕机时对应插槽可以转移到活着的节点。
问题:如何将同一类的数据固定的保存在同一个redis实例?
使用相同{key}。

集群伸缩

添加一个节点到集群:redis -cli --cluster add -node
需求:向集群中添加一个新的master节点,并向其中存储num=10

  1. 启动一个新的redis实例,指定端口:redis-server port/redis.conf
  2. 添加实例端口到之前的集群,作为一个master节点:redis-cli --cluster add-node ip:port ip:port(已存在的)
  3. 给实例端口节点分配插槽,使得num这个key可以存储到实例端口(难点):redis-cli --cluster reshard ip:port(已存在的) num(插槽数量) 然后根据提示配置好插槽即可。
    练习:删除集群中的一个节点。
故障转移

当集群中有一个master宕机后会发生什么事情?(自动)

  1. 首先该实例与其他实例失去连接
  2. 然后疑似宕机
  3. 最后确定下线,自动提升一个slave为新的master。

数据转移(手动)
利用cluster failover命令可以手动让集群中的某个master宕机,切换到cluster failover命令的这个slave节点,实现无感知的数据迁移。《图:分片集群手动数据迁移》
Failover三种模式:缺省,force,takeover

案例:在7002这个slave节点执行手动故障转移,重回master低位:

  1. 利用redis-cli连接7002
  2. 执行cluster failover命令
RedisTemplate访问分片集群

RedisTemplate底层同样基于lettuce实现了分片集群的支持而使用的步骤和哨兵模式基本一致:

  1. 引入redis的starter依赖
  2. 配置分片集群地址
  3. 配置读写分离

多级缓存-亿级流量的缓存方案

传统缓存的问题:传统缓存策略一般是请求到tomcat后,先查询redis,如果未命中则查询数据库。

  • 请求要经过tomcat处理,tomcat性能不如redis成为整个系统的瓶颈
  • redis缓存失效,对数据库产生冲击。

多级缓存方案:利用处理请求的每个环节,分别添加缓存,减轻Tomcat压力。
用户–浏览器端缓存(静态资源渲染:检验码304,90%请求可拦截)–nginx(反向代理)–nginx本地缓存(集群)–nginx端redis缓存–tomcat进程缓存–mysql

JVM进程缓存(tomcat进程缓存)

缓存分为两类:

  • 分布式缓存(Redis):优点:量大 ,可靠性高,可以集群。缺点:有网络开销。场景:数据量大,可靠性高,集群间共享。
  • 本地进程缓存(Caffeine,HashMap,GuavaCache):优点:本地,无网络,速度快。缺点:量小,可靠性低,无法共享。场景:性能要求高,数据量小

初识Caffeine:java8开发Spring内部缓存使用(本地缓存中最优)。Cache<String, String> cache = Caffeine.newBuilder().build();利用工厂模式构造cache对象。
Caffine缓存驱逐策略:基于容量,基于时间,基于引用(性能差,不建议)。在一次读写操作后或空闲时间完成数据驱逐。

Lua语法入门(nginx+Lua)

Lua:标准C编写的脚本语言,用于嵌入应用程序中,为应用程序提供灵活的扩展和定制功能。
Lua数据结构:nil,boolean,number,string,function(c或Lua编写的函数),table(hashmap)。可用type函数返回变量类型。
Lua定义变量:local str = ‘test’,local arr = {‘java’,‘py’},local map = {name = ‘Jack’,age = 32}
Lua获取变量:arr1,map[‘name’],map.name
Lua遍历数组:for index,value in ipairs(arr) do //todo end
Lua遍历table:for key,value in pairs(map) do //todo end
Lua函数:function 函数名(arg1,arg2…) //函数体 return nil end
Lua条件控制:类似javaif,else

实现多级缓存

OpenResty:基于Nginx的高性能web平台。用于搭建处理高并发,扩展性高的动态web应用,web服务和动态网关。功能:

  • 具备Nginx的完整功能
  • 基于Lua语言进行扩展,集成了大量精良的Lua库,第三方模块
  • 允许使用Lua自定义业务逻辑,自定义库

OpenResty构思:前端请求被nginx反向代理到虚拟机OpenResty集群,请求在OpenResty中接收这个请求,并返回数据。

OpenResty流程:

  1. 修改nginx.conf文件:在http下面添加对OpenResty的Lua模块的加载;在server下面添加对path路径的监听(响应类型和响应数据)。
  2. 编写item.lua文件
    OpenResty获取请求参数:《图:OpenResty获取请求参数》
nginx向tomcat获取数据(暂不加redis缓存)

nginx内部发送Http请求:

local resp = ngx.location.capture("/path",{
    method = ngx.HTTP_GET,  --请求方式 
    args = {a=1,b=2},  --get方式传参
    body = "c=3&d=4"  -- post方式传参
})
//注意:这里的/path是路径,并不包含ip和端口。这个请求会被nginx内部的server监听并处理
暂时不经过redis缓存,直接向tomcat服务器访问获取数据,还需一个server对这个路径做反向代理:
location /path {
    #这里是windows电脑的ip和java服务端口,需确保windows防火墙处关闭状态
    proxy_pass http://192.168.150.1:8081
}

返回响应内容:resp.status(状态码),resp.header(响应头,table),resp.body(响应数据)
封装http查询的函数:我们可以把http查询请求封装为一个函数,放到openresty函数库中,方便后面使用。

  1. 在/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查询失败, path: ",path,",args:",args)
        ngx.exit(404)
    end
    return resp.body
end
--将方法导出为table
local _M = {
    read_http = read_http
    read_redis = read_redis //预留:后面需要加载redis模块
}
return _M

使用http查询函数:

--导入封装的common函数
local common = require('common')
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 id = ngx.var[1]
--查询商品信息
local itemJSON = read_http("/item/" .. id, nil)
--查询库存信息
local stockJSON = read_http("/iem/stock/" .. id, nil)
--JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
--组合数据
item.stock = stock.stock
item.sold = stock.sold
--把item序列化为json返回结果
ngx.say(cjson.encode(item))

JSON处理结果:
OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。

  • 引入json模块: 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)

tomcat端做负载均衡:
每个tomcat的实例缓存都在本地,需要做hash映射。《图:tomcat集群负载均衡》

upstream tomcat-clusrer{
    hash $request_uri;  --基于requestUri做hash映射
    server ip:port1
    server ip:port2
}
server{
    listen  8081;
    server_name = loclahost;
    location / item {
        proxy_pass http://tomcat-cluster
    }
    location ~ /api/item/(\d+) {
        #默认的响应类型
        default_type application/json
        #响应结果由lua/item.lua文件决定
        content_by_lua_file lua/item.lua
    }
}
nginx加redis缓存

添加Redis缓存(冷启动与缓存预热):

  • 冷启动:启动服务时Redis没有缓存,如果第一次加载全量数据缓存,会给数据库带来压力
  • 缓存预热:利用大数据统计用户访问热点数据,在启动时就将这些数据加载到Redis中
@Component
public class RedisHandler implements InitializingBean {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private Service service;
    private static final ObjectMapper MAPPER = new ObjectMapper(); //json序列化
    @Override
    public void afterPropertiesSet() throws Exception{
        //初始化缓存
        //1.查询信息
        List<Bean> list = service.list();
        //2.放入缓存
        for(Bean bean : list){
            String json = MAPPER.writeValueAsString(bean);
            redisTemplate.opsForValue.set("KEY",json);
        }
    }
}

OpenResty加载Redis模块,放入common模块,并在lcoal _M{}中将read_redis方法暴露出去:

  • 引起如Redis模块,初始化Reids对象:
local redis = require('resty.redis')
local red = redis::new()
red:set_timeout(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连接池失败:', e)
    end
end
  • 封装函数,从Redis读取数据并返回
local function read_redis(ip, port, key)
    local ok,err = red:connect(ip,port)  --获取一个连接
    if not ok then 
        ngx.log(ngx.ERR, "连接Redis失败:", e)
        return nil
    end
    //查
    lcoal resp,err = red.get(key)
    if not resp then
        ngx.log(ngx.ERR, "查询Redis失败:", e)
    end
    if resp == ngx.null then 
        resp = nil;
        ngx.log(ngx.ERR, "查询Redis为空:", e)
    end
    closr_redis(red)
    return resp
end
  • 封装函数,查询加载Redis
local function read_data(key, path, params)
    localresp = read_redis("127.0.0.1", 6379, key)
    if not resp then 
        resp = read_http(path, params)
    end
    return resp
end
nginx本地缓存

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

  • 开启共享字典:在nginx.conf的http下添加配置:lua_shared_dict item_cache 150m;
  • 操作共享字典:
    local item_cache = ngx.shared.item_cache
    item_cache:set(‘key’,‘value’,1000)
    local var = item_cache:get(‘key’)

需求:1.修改item.lua中的read_data函数,优先查询本地缓存,未命中再查redis,tomcat。2.查redis或tomcat写入本地设置有效期。

缓存同步策略

  1. 设置有效期:给缓存设置有效期,到期自动查询,再次查询时更新。(差,缓存不一致,更新频率低)
  2. 同步双写:修改数据库同时修改缓存。(强,一致,有代码入侵,耦合度高)
  3. 异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据。(一般,中间状态不一致,可通知多个服务)
基于canal的异步通知

canal基于数据库增量日志解析,提供增量数据订阅&消费。可监听mysql数据库binlog通知缓存服务,更新redis,代码侵入低。
canal基于mysql的主从同步实现。

  1. mysql master将数据变更写入二进制日志(binary log),其中数据为binary log events
  2. mysql slave将master的binary log events拷贝到他的中继日志(relay log)
  3. mysql slave重放relay log中事件,将数据同步。

canal就是把自己伪装成mysql的一个slave节点,从而监听master的binlog变化。
开启canal需要修改mysql主从。

Canal客户端
坐标:<artifactId>canal-spring-boot-starter</artifactId>
配置:
canal:
    destination: xuy
    server: ip:port

编写监听类,监听canal:
canal推送给canal-client的是被修改的这一行数据,其会把行数据转换成Item实体类。需要在字段上加@TableName,@Id,@Column,@TableField(exist=false)+@Transient(非数据库字段)等注解

/**
 * 监听增,改,删的消息
*/
@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}
    @Override
    public void update(Item before, Item after) {//更新reids,本地缓存}
    @Override
    public void delete(Item item) {//删除reids,本地缓存}
}

多级缓存架构图

《图:多级缓存sum》

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis 是一种高性能的内存键值存储系统,常用作分布式缓存。它支持多种数据结构,如字符串、哈希、列表、集合和有序集合,并提供了丰富的操作命令和功能。 在分布式缓存中,Redis 可以通过搭建 Redis 集群来实现高可用和扩展性。Redis 集群使用哈希槽分片的方式将数据分散存储在多个节点上。每个节点负责管理一部分哈希槽,并与其他节点进行数据交互和同步。这样,当需要查询或写入数据时,客户端会根据键的哈希值将请求发送到相应的节点上,从而实现分布式的数据存储和访问。 使用 Redis 分布式缓存可以带来以下好处: 1. 高性能:Redis 的数据存储在内存中,读写速度非常快,适用于对响应时间要求较高的场景。 2. 高可用性:通过搭建 Redis 集群,即使某个节点发生故障,系统仍然可以继续正常工作。 3. 扩展性:可以根据需求增加节点数量,实现横向扩展,提高系统的处理能力。 4. 数据持久化:Redis 支持将数据持久化到磁盘,以防止数据丢失。 当使用 Redis 分布式缓存时,需要注意以下事项: 1. 数据一致性:由于 Redis 集群会将数据分片存储在不同节点上,需要确保数据的一致性,可以使用一致性哈希算法来解决这个问题。 2. 故障处理:当某个节点发生故障时,需要及时进行故障转移,将故障节点的数据迁移到其他正常节点上。 3. 客户端的负载均衡:需要在客户端实现负载均衡的策略,将请求均匀地分发到不同的节点上,以提高系统的整体性能。 总之,Redis 分布式缓存提供了高性能、高可用性和可扩展性的解决方案,可以用于加速应用程序的数据访问并提高系统的吞吐量。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值