redis持久化、主从和哨兵架构

一、redis持久化

1、RDB快照(snapshot)

redis配置RDB存储模式,修改redis.conf文件如下配置:

# 在300s内有100个或者以上的key被修改就会把redis中的数据持久化到dump.rdb文件中
# save 300 100

# 配置数据存放目录(现在指定的是redis目录下的data文件夹)
dir ./data

# 配置rdb文件存储文件名
dbfilename dump.rdb

也可以在连接redis时手动输入save或者bgsave命令都会将所有redis内存快照生成到一个新的dump.rdb文件,并覆盖原来的rdb文件

bgsave写时复制(COW)机制

主线程在执行redis命令,在内存中生成副本的同时由主线程fork出来的子进程开始读取redis内存中的数据并且写到rdb文件中,fork出来的bgsave子进程和主线程互不影响(redis后台生成rdb文件是采用bgsave方式)

save和bgsave

  1. save是和主线程同步执行,bgsave是异步执行
  2. save会阻塞其它命令,bgsave在主线程fork时会有短暂的阻塞
  3. save不会额外消耗内存,bgsave不会阻塞客户端命令

|---------------|----------|----------------------------|
| 命令 | save | bgsave |
| IO类型 | 同步 | 异步 |
| 是否阻塞redis其它命令 | 是 | 否(在生成子进程执行调用fork函数时会有短暂阻塞) |
| 复杂度 | O(n) | O(n) |
| 优点 | 不会消耗额外内存 | 不阻塞客户端命令 |
| 缺点 | 阻塞客户端命令 | 需要fork子进程,消耗内存 |

2、AOF(append-only file)

rdb的缺点也非常明显,不是实时持久化数据到rdb文件,如果系统宕机,redis会丢失最近时间的数据。AOF持久化是将修改的每一条指令报存到appendonly.aof文件中(和mysql的redolog一样先写os cache操作系统缓存,每隔一段时间sync到磁盘)

开启aof

修改redis.conf文件如下配置:

# 开启aof
appendonly yes
# aof文件名
appendfilename "appendonly.aof"

# aof三种策略
# 每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
# appendfsync always
# 每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
# appendfsync everysec
# 从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。
# appendfsync no

执行set gaorufeng 123 ex 100 命令生成的aof文件如下(星号后面的数字代表命令有多少个参数,$号后面的数字代表这个参数有几个字符,如果执行带过期时间的set命令,aof文件里记录的是并不是执行的原始命令,而是记录key过期的时间戳)

*5
$3
SET
$9
gaorufeng
$3
123
$4
PXAT
$13
1693895615978
AOF重写

比如一条自增命令,在生成aof文件就会生成多条指令,incr count命令的aof文件如下:

*2
$4
incr
$5
count
*2
$4
incr
$5
count
*2
$4
incr
$5
count
*2
$4
incr
$5
count

重写的作用旨在减少指令的存储,可以配置aof让redis重写,修改redis.conf文件如下配置

# 当前文件是之前文件大小的两倍(设置100),自动触发重写
auto-aof-rewrite-percentage 100
# aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
auto-aof-rewrite-min-size 64mb

redis客户端执行bgrewriteaof手动重写aof,此时命令如下:

*3
$3
SET
$5
count
$1
4

aof方式存储数据完整,rdb存储数据文件小,故启动redis时当选rdb恢复内存数据,二进制恢复数据快

|-------|-------|--------|
| 命令 | RDB | AOF |
| 启动优先级 | 低 | 高 |
| 体积 | 小 | 大 |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 容易丢数据 | 根据策略决定 |

3、混合持久化

# 开启混合持久化
aof-use-rdb-preamble yes

在开启aof的前提下,aof重写前内存做rdb快照处理,并且将rdb快照内容和增量的aof修改内存数据的命令存在一起

Redis数据备份策略:

  1. 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份
  2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
  3. 每次copy备份的时候,都把太旧的备份给删了
  4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏

二、redis主从架构

redis主从架构搭建,配置从节点步骤:

java 复制代码
1、进入到redis目录下,创建数据存储目录
mkdir data
cd data
mkdir 6379
mkdir 6380
mkdir 6381

2、copy不同配置文件
cp -r redis.conf redis.conf.6379
cp -r redis.conf redis.conf.6380
cp -r redis.conf redis.conf.6381

3、(6379为主节点,其余为从节点)修改配置文件
vim redis.conf.6379
在6379配置文件中修改如下
port 6379
daemonize yes
protected-mode no
pidfile /var/run/redis_6379.pid
logfile "6379.log"
dir /usr/local/redis/redis-6.2.7/data/6379
# 需要注释掉bind
# bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)

4、配置从节点6380
vim redis.conf.6380
在6380配置文件中修改如下
port 6380
daemonize yes
protected-mode no
pidfile /var/run/redis_6380.pid
logfile "6380.log"
dir /usr/local/redis/redis-6.2.7/data/6380
# 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replicaof 192.168.146.128 6379
# 配置从节点只读
replica-read-only yes

5、配置从节点6381
vim redis.conf.6381
在6381配置文件中修改如下
port 6381
daemonize yes
protected-mode no
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dir /usr/local/redis/redis-6.2.7/data/6381
# 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replicaof 192.168.146.128 6379
# 配置从节点只读
replica-read-only yes

6、先启动主节点6379,后启动从节点6380、6381
src/redis-server redis.conf.6379
src/redis-server redis.conf.6380
src/redis-server redis.conf.6381

7、连接从节点查看是否能获取主节点数据
src/redis-cli -p 6379
src/redis-cli -p 6381
Redis主从工作原理

如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC命令给master请求复制数据。

master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。

当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。

主从复制(全量复制)流程图:

数据部分复制

当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据,slave与master能够在网络连接断开重连后只进行部分数据复制(断点续传)。

master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。

主从复制(部分复制,断点续传)流程图:

如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点压力过大),可以做如下架构,让部分从节点与从节点(与主节点同步)同步数据

Jedis连接代码示例:

XML 复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
java 复制代码
public class JedisSingleTest {

	public static void main(String[] args) {
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxTotal(20);
		jedisPoolConfig.setMaxIdle(10);
		jedisPoolConfig.setMinIdle(5);
		
		// timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.146.128", 6379, 3000, null);
        
        Jedis jedis = null;
        try {
            //从redis连接池里拿出一个连接执行命令
            jedis = jedisPool.getResource();

            System.out.println(jedis.set("single", "gao"));
            System.out.println(jedis.get("single"));

            //管道示例
            //管道的命令执行方式:cat redis.txt | redis-cli -h 127.0.0.1 -a password - p 6379 --pipe
            Pipeline pl = jedis.pipelined();
            for (int i = 0; i < 10; i++) {
                pl.incr("pipelineKey");
                pl.set("gao" + i, "gao");
            }
            List<Object> results = pl.syncAndReturnAll();
            System.out.println(results);

            //lua脚本模拟一个商品减库存的原子操作
            //lua脚本命令执行方式:redis-cli --eval /tmp/test.lua , 10
            jedis.set("product_count_10016", "15");  //初始化商品10016的库存
            String script = " local count = redis.call('get', KEYS[1]) " +
                            " local a = tonumber(count) " +
                            " local b = tonumber(ARGV[1]) " +
                            " if a >= b then " +
                            "   redis.call('set', KEYS[1], a-b) " +
                            "   return 1 " +
                            " end " +
                            " return 0 ";
            Object obj = jedis.eval(script, Arrays.asList("product_count_10016"), Arrays.asList("10"));
            System.out.println(obj);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null)
                jedis.close();
        }
	}
}

管道(Pipeline)

客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。

pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信息;也就是pipeline并不是表达"所有command都一起成功"的语义,管道中前面命令失败,后面命令不会有影响,继续执行。

三、redis哨兵高可用架构

sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。

哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)

基于主从架构的redis哨兵架构搭建步骤:

XML 复制代码
1、复制一份sentinel.conf文件
cp sentinel.conf sentinel-26379.conf

2、将相关配置修改为如下值:
port 26379
daemonize yes
pidfile "/var/run/redis-sentinel-26379.pid"
logfile "26379.log"
dir "/usr/local/redis-5.0.3/data"
# sentinel monitor <master-redis-name> <master-redis-ip> <master-redis-port> <quorum>
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效
sentinel monitor mymaster 192.168.146.128 6379 2   # mymaster这个名字随便取,客户端访问时会用到

3、启动sentinel哨兵实例
src/redis-sentinel sentinel-26379.conf

4、查看sentinel的info信息
src/redis-cli -p 26379
127.0.0.1:26379>info
可以看到Sentinel的info里已经识别出了redis的主从

5、可以自己再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改

sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的最下面),我们查看下如下配置文件sentinel-26379.conf,如下所示:

XML 复制代码
sentinel known-replica mymaster 192.168.146.128 6380 #代表redis主节点的从节点信息
sentinel known-replica mymaster 192.168.146.128 6381 #代表redis主节点的从节点信息
sentinel known-sentinel mymaster 192.168.146.128 26381 c3c89b0c5d1b81f5e9487ae17260c8fbfe477c2e  #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.146.128 26380 b3d47687e6b2d6d569b0c6a53a1b92be5c442c5c  #代表感知到的其它哨兵节点

当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件的集群元数据信息,比如6379的redis如果挂了,假设选举出的新主节点是6380,则sentinel文件里的集群元数据信息会变成如下所示:

XML 复制代码
sentinel known-replica mymaster 192.168.146.128 6380 #代表主节点的从节点信息
sentinel known-replica mymaster 192.168.146.128 6379 #代表主节点的从节点信息
sentinel known-sentinel mymaster 192.168.146.128 26381 c3c89b0c5d1b81f5e9487ae17260c8fbfe477c2e  #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.146.128 26379 af83ca977cadbe5842472fea925c85906c26e1e0  #代表感知到的其它哨兵节点

同时还会修改sentinel文件里之前配置的mymaster对应的6379端口,改为6380

XML 复制代码
sentinel monitor mymaster 192.168.146.128 6381 2

当6379的redis实例再次启动时,哨兵集群根据集群元数据信息就可以将6379端口的redis节点作为从节点加入集群

哨兵的Jedis连接代码:

java 复制代码
public class JedisSentinelTest {
    public static void main(String[] args) throws IOException {

        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);

        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add(new HostAndPort("192.168.0.60",26379).toString());
        sentinels.add(new HostAndPort("192.168.0.60",26380).toString());
        sentinels.add(new HostAndPort("192.168.0.60",26381).toString());
        //JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池
        //JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其建立连接
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);
        Jedis jedis = null;
        try {
            jedis = jedisSentinelPool.getResource();
            System.out.println(jedis.set("sentinel", "gao"));
            System.out.println(jedis.get("sentinel"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null)
                jedis.close();
        }
    }
}

1、引入相关依赖:

XML 复制代码
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-pool2</artifactId>
</dependency>

springboot项目核心配置:

XML 复制代码
server:
  port: 8080

spring:
  redis:
    database: 0
    timeout: 3000
    sentinel:    #哨兵模式
      master: mymaster #主服务器所在集群名称
      nodes: 192.168.146.128:26379,192.168.146.128:26380,192.168.146.128:26381
      lettuce:
        pool:
          max-idle: 50
          min-idle: 10
          max-active: 100
          max-wait: 1000

访问代码:

java 复制代码
@RestController
public class IndexController {

    private static final Logger logger = LoggerFactory.getLogger(IndexController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
     * 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
     * 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip
     *
     * @throws InterruptedException
     */
    @RequestMapping("/test_sentinel")
    public void testSentinel() throws InterruptedException {
        int i = 1;
        while (true){
            try {
                stringRedisTemplate.opsForValue().set("gao"+i, i+"");
                System.out.println("设置key:"+ "gao" + i);
                i++;
                Thread.sleep(1000);
            }catch (Exception e){
                logger.error("错误:", e);
            }
        }
    }
}

StringRedisTemplate与RedisTemplate详解

spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。在RedisTemplate中提供了几个常用的接口方法的使用,分别是:

java 复制代码
private ValueOperations<K, V> valueOps;
private HashOperations<K, V> hashOps;
private ListOperations<K, V> listOps;
private SetOperations<K, V> setOps;
private ZSetOperations<K, V> zSetOps;

RedisTemplate中定义了对5种数据结构操作

java 复制代码
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set

StringRedisTemplate继承自RedisTemplate,也一样拥有上面这些操作。

StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。

RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

Redis客户端命令对应的RedisTemplate中的方法列表:

|------------------------------------------|--------------------------------------------------------|
| String类型结构 ||
| Redis | RedisTemplate rt |
| set key value | rt.opsForValue().set("key","value") |
| get key | rt.opsForValue().get("key") |
| del key | rt.delete("key") |
| strlen key | rt.opsForValue().size("key") |
| getset key value | rt.opsForValue().getAndSet("key","value") |
| getrange key start end | rt.opsForValue().get("key",start,end) |
| append key value | rt.opsForValue().append("key","value") |
| | |
| Hash结构 ||
| hmset key field1 value1 field2 value2... | rt.opsForHash().putAll("key",map) //map是一个集合对象 |
| hset key field value | rt.opsForHash().put("key","field","value") |
| hexists key field | rt.opsForHash().hasKey("key","field") |
| hgetall key | rt.opsForHash().entries("key") //返回Map对象 |
| hvals key | rt.opsForHash().values("key") //返回List对象 |
| hkeys key | rt.opsForHash().keys("key") //返回List对象 |
| hmget key field1 field2... | rt.opsForHash().multiGet("key",keyList) |
| hsetnx key field value | rt.opsForHash().putIfAbsent("key","field","value" |
| hdel key field1 field2 | rt.opsForHash().delete("key","field1","field2") |
| hget key field | rt.opsForHash().get("key","field") |
| | |
| List结构 ||
| lpush list node1 node2 node3... | rt.opsForList().leftPush("list","node") |
| lpush list node1 node2 node3... | rt.opsForList().leftPushAll("list",list) //list是集合对象 |
| rpush list node1 node2 node3... | rt.opsForList().rightPush("list","node") |
| rpush list node1 node2 node3... | rt.opsForList().rightPushAll("list",list) //list是集合对象 |
| lindex key index | rt.opsForList().index("list", index) |
| llen key | rt.opsForList().size("key") |
| lpop key | rt.opsForList().leftPop("key") |
| rpop key | rt.opsForList().rightPop("key") |
| lpushx list node | rt.opsForList().leftPushIfPresent("list","node") |
| rpushx list node | rt.opsForList().rightPushIfPresent("list","node") |
| lrange list start end | rt.opsForList().range("list",start,end) |
| lrem list count value | rt.opsForList().remove("list",count,"value") |
| lset key index value | rt.opsForList().set("list",index,"value") |
| | |
| Set结构 ||
| sadd key member1 member2... | rt.boundSetOps("key").add("member1","member2",...) |
| sadd key member1 member2... | rt.opsForSet().add("key", set) //set是一个集合对象 |
| scard key | rt.opsForSet().size("key") |
| sidff key1 key2 | rt.opsForSet().difference("key1","key2") //返回一个集合对象 |
| sinter key1 key2 | rt.opsForSet().intersect("key1","key2")//同上 |
| sunion key1 key2 | rt.opsForSet().union("key1","key2")//同上 |
| sdiffstore des key1 key2 | rt.opsForSet().differenceAndStore("key1","key2","des") |
| sinter des key1 key2 | rt.opsForSet().intersectAndStore("key1","key2","des") |
| sunionstore des key1 key2 | rt.opsForSet().unionAndStore("key1","key2","des") |
| sismember key member | rt.opsForSet().isMember("key","member") |
| smembers key | rt.opsForSet().members("key") |
| spop key | rt.opsForSet().pop("key") |
| srandmember key count | rt.opsForSet().randomMember("key",count) |
| srem key member1 member2... | rt.opsForSet().remove("key","member1","member2",...) |

相关推荐
blammmp8 分钟前
Java:数据结构-枚举
java·开发语言·数据结构
58沈剑18 分钟前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构
暗黑起源喵26 分钟前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong31 分钟前
Java反射
java·开发语言·反射
ketil2737 分钟前
Ubuntu 安装 redis
redis
九圣残炎1 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge1 小时前
Netty篇(入门编程)
java·linux·服务器
Re.不晚2 小时前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐2 小时前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。2 小时前
Spring Boot 配置文件
java·spring boot·后端