Redis学习记录

为什么要用Redis。

场景:网站服务器用tomcat运行起来后发现单个tomcat保存session无法满足如此庞大的用户量,或者所内存不足,我们采用多个tomcat来均衡负载,但是这就会造成session丢失,tomcat提出过多台tomcat共享session,但是这种方式基于拷贝,显然无法根本上解决问题。

解决:需要一种tomcat能够共同访问且存于内存的key-value形式存储辅助工具——redis。

Redis如何保存信息?

key没有结构变动,可以用指定的id也可用随机的token来作为key。

主要是value,通常value可以是json字符串也可以用哈希结构将每个字段单独存储。

PS:值得注意的是,Spring项目里用到redis的实例对象将信息存入redis时需要保证存入数据每一项都为字符串类型。
而我们获取的数据还有从数据库读出的数据都一般是多种类型的,这时我们需要手动或使用hutool包来帮助我们完成类型的转换。

缓存,为什么要用到缓存?

缓存作为数据交换的缓冲区,可以提高整个系统的运行效率降低后端的负载降低响应时间,不光在cpu和内存之间有cache缓存,web开发的每一级都有缓存存在。

但是同时存在以下四个问题
1.缓存穿透。
2.缓存击穿。
3.缓存雪崩。
4.redis与数据库一致性问题。

实例1.将数据库数据写入缓存

以标准的Spring框架为例。

我们在整个数据请求上的流程应该是:前端发送请求——后端Controller接收请求——Controller调用对应接口中方法——接口方法具体实现于对应Service——Service首先查询Redis中是否存在所查询的数据(存在则返回)——若不存在则向数据库中查找——数据库中找到后写入Redis(找不到则报错)——Redis返回所请求的数据的Json字符串。

缓存的更新策略,redis与数据库的一致性问题

Redis学习记录_第1张图片
虎哥讲的很好
这里注意超时剔除的一致性是根据你的TTL设置会变化的,TTL越长则一致性越好

Redis学习记录_第2张图片
这里第一种方式编码量大但可控性和一致性是最好的,所以目前多用这种方式
Redis学习记录_第3张图片
关于写后删和删后写的两种不一致情况
但是由于方案2发生不一致的概率较小,所以我们通常使用方案2
Redis学习记录_第4张图片
最后总结出最佳的方案为:
Redis学习记录_第5张图片

缓存穿透

缓存穿透是指用户访问数据库和redis中都没有的数据时请求总会直达数据库,当多个线程总是并发访问的时候就会造成数据库压力过大导致数据库崩溃

对应的解决方案有两种,如下是第一种,我们应对这种请求直接放回一个null,从而让redis可以拦住这种请求。
注意这个缓存null需要设置TTL

但是如果我们在null缓存生效的时候正好有一条对应的数据插入数据库,这时会造成不一致性,所以我们还需要进行缓存更新,主动去更新redis的对应缓存。
Redis学习记录_第6张图片
对于布隆过滤,由于实现困难且无法可靠解决击穿问题,所以我们通常使用缓存空对象这种方案。
Redis学习记录_第7张图片

缓存雪崩

Redis学习记录_第8张图片

缓存击穿

Redis学习记录_第9张图片

Redis学习记录_第10张图片
Redis学习记录_第11张图片

互斥锁解决缓存击穿问题

这里采用redis的提供的stringRedisTemplate.opsForValue().setIfAbsent(),规定每一个线程在发现redis没有对应的查询数据时,必须获取一个“资源”,那么这里则是让对应访问数据的id+前缀来作为key值,如果redis中不存在这个key值则set并返回true否则返回false,获得false返回则让进城继续等待,获得true返回则让进城开始向数据库中查询。

场景:商品限量抢购

ID生成问题

我们要保证ID满足全局唯一并且有一定的规律还不能被很容易的察觉出。

这时我们需要提供一个ID的生成方法,这里的思路就是采用时间戳+序列号的方法来实现。
在多进程并发的情况下,时间戳几乎没有规律,而序列号可以保证全局ID唯一。

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisWorker {

    /**
     * 开始时间戳
     */
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final int COUNT_BITS = 32;

    private static final long BEGIN_TIMESTAMP = 1640995200L;
    //全局ID生成方法——
    //生成的ID为时间戳31位+序列号32位
    public long nextId(String keyPrefix){
        //生成时间戳
        LocalDateTime now =LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond-BEGIN_TIMESTAMP;
        //生成序列号
        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        //这里解释一下为什么要再拼接一个时间戳,因为同一个业务一天内几乎不可能达到10亿下单,但是长期却很有可能让同一个key达到2^32-1的上限
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回,位运算
        return timestamp << COUNT_BITS | count;
    }

}

超卖问题:悲观和乐观锁

Redis学习记录_第12张图片
悲观锁:我们可以参照之前的互斥锁来实现,只要一个进程把拿住资源,其他进程都无法拿到

乐观锁的两种实现方案:

实际上都是在数据库上的sql语句上做手脚,加之判断条件而已,第一种方法版本号就是新加一个version作为判断条件, 如果线程查询到的version等于即将修改时的version则可以修改。

比较法则是将版本号法的version变换为stock而已。

但是注意了,直接判断stock=stock会造成极高的失败率,因为线程不会循环的去访问

所以我们这里让stock>0作为条件即可

Redis学习记录_第13张图片

Redis学习记录_第14张图片

一人一单问题

Redis学习记录_第15张图片
在单机情况下,我们可以使用一个synchronized方法来实现对于修改数据库这个事物加锁,只要我们注意好1.synchronized作用域不要过大过小 2.不要让spring的事物失效 即可

但是通常我们的项目上线是集群运行。
一人一单问题,考虑到集群下的并发问题,我们不能用JVM内部的锁监视器,需要一个共同的锁监视器,使得让所有集群内的jvm都可以被同一个锁监视器控制。这就引出了——分布式锁

分布式锁

Redis学习记录_第16张图片
Redis学习记录_第17张图片
Redis学习记录_第18张图片

分布式锁的实现
分布式锁的误删问题

误删问题提出这样一个场景。

线程1获取锁之后便开始阻塞,阻塞时长超过锁的超时删除上限

线程2此时获取锁,线程1结束阻塞,业务完成,释放锁
线程3此时因为线程1把线程2的锁删除,则也可以获取锁并执行业务,并发安全再次出现。

解决方式也简单:获取一个锁的标识,这里用UUID+ThreadId来解决。
Redis学习记录_第19张图片

分布式锁的原子性问题

原子性问题提出了下面这样一个场景。

线程1开始执行业务,获取互斥锁,修改完数据库并且确认或线程ID之后线程1阻塞

线程2在线程1阻塞之后获取锁开始执行业务

线程1结束阻塞,并且将线程2的锁删除了

线程3此时介入,获取锁并开始执行业务。又一次发生了并发危险

这就要求,识别锁是否可删除和删除锁必须是一个动作,不可拆分。
Redis学习记录_第20张图片

方案1:采用Redis内部事务+乐观锁

由于Redis事务是等到最后一起执行的,在一开始获取不到ThreadId标识,这样就又会出现一致性问题,所以我们只能在删除的时候配合用乐观锁来做,这样才能保证别人没有修改的情况下我去删锁,但是这样太过复杂。

方案2:Lua脚本

Lua脚本是由Lua语言编写的,Redis可支持的一种可编入多条redis指令并且确保一次性执行的脚本工具,这样就可以保证原子性。

Redis学习记录_第21张图片
Redis学习记录_第22张图片

使用Redisson实现分布式锁

基于Setnx还是有如下问题

1.不可重入

同一个线程无法多次获取同一把锁,同一个线程自己和自己的业务也会被迫串行。

Redssion的trylock可以做到。

原理:将原本的setnx的k-v结构的v从字符串改为哈希。

当线程初次获取锁,我们将哈希的v值+1,当同一线程再次获取锁,则将v值再加1,并且设置存活期

当线程要释放锁,我们直接将v值-1而不是直接释放锁,直到v值-1变为0才可以真的释放锁
Redis学习记录_第23张图片
注意:依然要考虑获取锁和释放锁的原子性问题,所以不能直接放到java中实现,需要使用LUA脚本。

在redisson提供的源码中,我们发现时这样的一个类。
trylock脚本
Redis学习记录_第24张图片
unlock脚本

Redis学习记录_第25张图片

2.不可重试

获取锁只尝试一次就返回false,没有重试机制。

3.超时释放

超时释放虽然可以避免死锁,但是如果执行时间较长,则会出现锁的提前释放,有并发安全问题。
Redis学习记录_第26张图片
在Redisson中,不可重试和超时释放都可以解决,对于尝试获取锁的动作有三个参数可以传:waitTime,leaseTime,TimeUnit

对于正在使用锁的进程可以返回一个timeToLease的参数。

1.当进程尝试获取锁,如果timeToLease为null,说明没有进程拿到互斥锁,这时获取锁就成功了,然后根据你给定的leaseTime判断是否启动看门狗。

2.看门狗:如果你指定了锁的持续时间,那么就听你的,如果没有指定,那么看门狗就会递归的去不断刷新持续时间。

3.当进程获取锁失败,我们则会判断剩余的waitTime是否为0,如果为0则放弃获取锁,重试结束。如果不为0,则订阅一个“消息”,这个消息就是其他进程的锁释放消息。

4.当你要释放锁,需要先发送释放“消息”,然后取消看门狗。

PS:以至于你说看门狗无限刷新导致其他线程饿死,这不是中间件要考虑的,这时你java代码要考虑的。
java代码需要保证事务完成后即便时异常最后finally也要释放锁。

4.主从性一致

如果Redis提供里主从集群,主从同步存在延迟,主机无法完成写操作的时候,如果从并同步主中的锁数据,则会出现锁失效 。
Redis学习记录_第27张图片

这里redisson采用的思路是对于每一个可获取的锁的节点都要成功获取锁才能让锁的获取成功,否则就获取失败。每一个节点都会重复问题2,3解释时锁的获取流程。

业务优化,异步完成数据库修改

原本我们在完成秒杀业务的时候会有如下几个步骤:判断是否库存充足,判断是否一人一单,修改数据库,但是这个过程显然会让数据库压力过大,而redis的作用几乎完全没有,就是加了个锁。

这时我们想到可以拆分步骤:先让redis中存入对应的商品信息,并且在redis中完成购买资格判断和商品数量减少的动作,在秒杀结束后,多开一个线程出来,把redis中处理完的信息写入数据库。

Redis学习记录_第28张图片
实现如下:

1.在新增商品时将商品存入redis
这个步骤其实很简单,只要创建的时候搞一下就行了

2.基于LUA脚本,完成Redis中的购买资格判断,扣减库存

此业务是将原本在java中的判断移植到LUA脚本中,确保了原子性。这个和在java中实现直接对接数据库不一样,直接分布调用redis命令会造成并发安全问题。

3.如果抢购成功,将用户的ID和优惠券的ID存入阻塞队列

这个也不难,前面的业务确保了线程安全,这里生成优惠券信息的时候不用太多考虑。

4.开启线程,将阻塞队列中的内容写入数据库

开启子线程,将存入数据库的操作实现异步写入。但是值得注意的是,子线程是新开的线程,无法从线程池中获得父线程的代理对象和存入父线程的threadlocal的对象信息。只能提前获取对象然后提交给子线程。

基于消息队列完成订单存储

在高并发的情况下,我们基于jvm中的阻塞队列实现的存储是有两个严重的问题
1.空间不够,阻塞队列有空间的上限限制

2.当jvm关闭或者系统宕机时数据将全部丢失
Redis学习记录_第29张图片
1.消息队列在JVM以外,不受内存的限制。
2.消息队列对数据持久化,不会因为宕机而导致数据丢失。

消息队列有很多,比如kafka,rabbitmq,这里先学一下Redis自带的三种方式。

list结构

Redis学习记录_第30张图片

pubsub(发布订阅)

Redis学习记录_第31张图片

stream

这是Redis 5.0后中支持的一种全新数据类型和之前的数据类型类似支持数据持久化。
Redis学习记录_第32张图片

Redis学习记录_第33张图片

单点Redis的四大问题——分布式缓存

Redis学习记录_第34张图片

数据丢失问题——Redis持久化

RDB

RDB——Redis Database Backup file,也被叫做Redis数据快照。就是将内存中的数据存到磁盘中,当Redis重启后可以读取文件并且恢复数据。

以下是几个redis自身关于RDB的配置,如dir ./ 意义是RDB文件默认路径为redis安装路径
Redis学习记录_第35张图片

如下两种Redis的RDB方式。
save:由于Redis我们知道是单线程的,当主线程save命令执行的时候会阻塞其他命令,所以只推荐宕机或停机时使用。

bgsave:由主进程fork一个子进程,子进程与主进程共享内存数据。但是执行for时仍然会阻塞,所以我们需要让fork尽快完成。
Redis学习记录_第36张图片

RDB的bgsave底层实现细节。
如下:当主进程要bgsave的时候,会将自身的读取数据用到的页表(段表)复制一份给子进程,映射关系得到后就完成了数据的共享,并且在子进程读取备份时,数据被标记为read-only状态,避免了脏写脏读。

此时若主进程需要对某个数据进行写操作,则将对应的数据拷贝,然后单独进行写操作。这也是为什么redis要预留内存空间的原因之一,为了避免过多的拷贝导致内存不足。
Redis学习记录_第37张图片

AOF

上述RDB方式有一个缺点:两次RDB时间间隔长,中间还是会产生很多的数据,若这时宕机,仍然会数据丢失。

为了应对这个问题,我们采用AOF——Append Only File,将每一个写命令都保存在AOF中、

注意:写入内存和写入磁盘是一个不可分的过程,只有都处理完才会进行返回结果,所以此时宕机是数据写入失败,不是数据丢失
Redis学习记录_第38张图片
如下为redis对AOF的配置信息
Redis学习记录_第39张图片

但是显然,这样会有非常非常多的对同一个key的无效写命令,这时我们可以采用bgrewriteaof,可以让AOF重写,达到缩减文件大小的效果,bgrewriteaof不需要手动触发,只要满足了最小文件体积阈值和对比上次文件大小体积增加量阈值就可以触发bgrewriteaof。
Redis学习记录_第40张图片

小结
Redis学习记录_第41张图片

并发能力问题——主从集群

Redis学习记录_第42张图片

Cluster搭建

1.搭建三个独立节点(以下均为linux环境下使用)

要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。

1)创建目录

我们创建三个文件夹,名字分别叫700170027003:

```sh
# 进入/tmp目录
cd /tmp
# 创建目录
mkdir 7001 7002 7003

2)拷贝配置文件到每个实例目录

然后将redis-6.2.4/redis.conf文件拷贝到三个目录中(在/tmp目录执行下列命令):

# 方式一:逐个拷贝
cp redis-6.2.4/redis.conf 7001
cp redis-6.2.4/redis.conf 7002
cp redis-6.2.4/redis.conf 7003

# 方式二:管道组合命令,一键拷贝
echo 7001 7002 7003 | xargs -t -n 1 cp redis-6.2.4/redis.conf

3)修改每个实例的端口、工作目录

修改每个文件夹内的配置文件,将端口分别修改为7001、7002、7003,将rdb文件保存位置都修改为自己所在目录(在/tmp目录执行下列命令):

sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \/tmp\/7001\//g' 7001/redis.conf
sed -i -e 's/6379/7002/g' -e 's/dir .\//dir \/tmp\/7002\//g' 7002/redis.conf
sed -i -e 's/6379/7003/g' -e 's/dir .\//dir \/tmp\/7003\//g' 7003/redis.conf

4)修改每个实例的声明IP

虚拟机本身有多个IP,为了避免将来混乱,我们需要在redis.conf文件中指定每一个实例的绑定ip信息,格式如下:

# redis实例的声明 IP
replica-announce-ip 192.168.150.101

每个目录都要改,我们一键完成修改(在/tmp目录执行下列命令):

# 逐一执行
sed -i '1a replica-announce-ip 192.168.150.101' 7001/redis.conf
sed -i '1a replica-announce-ip 192.168.150.101' 7002/redis.conf
sed -i '1a replica-announce-ip 192.168.150.101' 7003/redis.conf

# 或者一键修改
printf '%s\n' 7001 7002 7003 | xargs -I{} -t sed -i '1a replica-announce-ip 192.168.150.101' {}/redis.conf

为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:

# 第1个
redis-server 7001/redis.conf
# 第2个
redis-server 7002/redis.conf
# 第3个
redis-server 7003/redis.conf

如果要一键停止,可以运行下面命令:

printf '%s\n' 7001 7002 7003 | xargs -I{} -t redis-cli -p {} shutdown

2.开启节点主从关系(从节点认主)

现在三个实例还没有任何关系,要配置主从可以使用replicaof 或者slaveof(5.0以前)命令。

有临时和永久两种模式:

  • 修改配置文件(永久生效)

    • 在redis.conf中添加一行配置:slaveof
  • 使用redis-cli客户端连接到redis服务,执行slaveof命令(重启后失效):

    slaveof  
    

注意:在5.0以后新增命令replicaof,与salveof效果一致。

这里我们为了演示方便,使用方式二。

通过redis-cli命令连接7002,执行下面命令:

# 连接 7002
redis-cli -p 7002
# 执行slaveof
slaveof 192.168.150.101 7001

通过redis-cli命令连接7003,执行下面命令:

# 连接 7003
redis-cli -p 7003
# 执行slaveof
slaveof 192.168.150.101 7001

然后连接 7001节点,查看集群状态:

# 连接 7001
redis-cli -p 7001
# 查看状态
info replication

测试

执行下列操作以测试:

  • 利用redis-cli连接7001,执行set num 123

  • 利用redis-cli连接7002,执行get num,再执行set num 666

  • 利用redis-cli连接7003,执行get num,再执行set num 888

可以发现,只有在7001这个master节点上可以执行写操作,7002和7003这两个slave节点只能执行读操作。

全量同步

在同步过程中,从节点有两个很重要的参数:Replication Id(replid)和offset。

Replication Id是数据集的标记,id一致则说明是同一数据集。每个master都有唯一的replid,从节点会同步数据时会继承这个id。

offset:随着记录在repl_baklog的数据增多而增大。slave同步repl_baklog时也会同步offset。

下图为主从第一次同步的过程。

已经很清楚了吧,,不太需要解释了
Redis学习记录_第43张图片

第三阶段后,主节点还是会不断地有新的指令,这些指令在都会写入repl_baklog中,然后开启子进程去传送给从节点,从节点应和主节点数据保证一致性

增量同步

增量同步是指在从节点宕机或者因为其他原因重启后,主从数据不一致,要求从节点将连接断开阶段的增量数据同步。

当repl_baklog中数据未写满覆盖最早的已同步数据时还是可以做增量同步的,如果覆盖了则会重新做全量同步。
Redis学习记录_第44张图片

由于全量同步是通过RDB文件实现的,需要磁盘的IO,而且文件也很大,显然对比增量同步会非常慢,我们要尽量避免这种问题的发生,如下是几个优化方案,思路分别为:禁止磁盘的io控制RDB文件大小,限制Redis的内存使用量控制RDB大小,提高repl_baklog大小避免使用RDB文件,主-从-从链式结构减少主节点的同步压力。

Redis学习记录_第45张图片

故障恢复问题——哨兵机制

哨兵机制的三个作用和实现原理;

1.监控:哨兵集群中的每一个节点会每隔1秒向master发送一个ping,如果在规定时间内没有返回pong,则认为其主观下线。当认为其主观下线的哨兵数量大于哨兵集群的节点数一半时认为其客观下线。

2.重新选出master节点:首先判断断开连接的时长,如果断开时间长于指定值则不选其为master,然后根据slave_priority选出值最大的从节点,如果从节点的该值一致,则选出offset值较大的节点,如果offset也一样,则id越小优先级越高。

3.重新搭建主从结构:先向选出的master发送"slave of no one"命令,然后向其他节点发送slave of ip的命令

哨兵集群的搭建

3.1.集群结构

这里我们搭建一个三节点形成的Sentinel集群,来监管之前的Redis主从集群。如图:

三个sentinel实例信息如下:

节点 IP PORT
s1 192.168.150.101 27001
s2 192.168.150.101 27002
s3 192.168.150.101 27003

3.2.准备实例和配置

要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。

我们创建三个文件夹,名字分别叫s1、s2、s3:

# 进入/tmp目录
cd /tmp
# 创建目录
mkdir s1 s2 s3

如图:

然后我们在s1目录创建一个sentinel.conf文件,添加下面的内容:

port 27001
sentinel announce-ip 192.168.150.101
sentinel monitor mymaster 192.168.150.101 7001 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
dir "/tmp/s1"

解读:

  • port 27001:是当前sentinel实例的端口
  • sentinel monitor mymaster 192.168.150.101 7001 2:指定主节点信息
    • mymaster:主节点名称,自定义,任意写
    • 192.168.150.101 7001:主节点的ip和端口
    • 2:选举master时的quorum值

然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中(在/tmp目录执行下列命令):

# 方式一:逐个拷贝
cp s1/sentinel.conf s2
cp s1/sentinel.conf s3
# 方式二:管道组合命令,一键拷贝
echo s2 s3 | xargs -t -n 1 cp s1/sentinel.conf

修改s2、s3两个文件夹内的配置文件,将端口分别修改为27002、27003:

sed -i -e 's/27001/27002/g' -e 's/s1/s2/g' s2/sentinel.conf
sed -i -e 's/27001/27003/g' -e 's/s1/s3/g' s3/sentinel.conf

3.3.启动

为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:

# 第1个
redis-sentinel s1/sentinel.conf
# 第2个
redis-sentinel s2/sentinel.conf
# 第3个
redis-sentinel s3/sentinel.conf

3.4.测试

尝试让master节点7001宕机,查看sentinel日志:

基于RedisTemplate来连接哨兵

如图三步走,特别注意第二步的sentinel模式下我们从主节点的名称来获取,并不能写死访问地址,主节点名称需要和linux构建时的名称一致。

第三步的读写分离也要注意@Bean的注释
Redis学习记录_第46张图片
Redis学习记录_第47张图片

存储能力问题——Redis分片集群

以上采用主从和哨兵可以解决高可用和高并发读取的问题。

但是关于:海量数据存储和高并发写问题还是没有得到解决。

分片集群的特征:
1.集群中有多个master。
2.每个master也会有slave节点
3.master之间互相监督,参考哨兵机制
4.客户端可以访问任意集群中节点,最终都会被路由正确节点

分片集群的构建

集群结构

分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含3个master节点,每个master包含一个slave节点,结构如下:

这里我们会在同一台虚拟机中开启6个redis实例,模拟分片集群,信息如下:

IP PORT 角色
192.168.150.101 7001 master
192.168.150.101 7002 master
192.168.150.101 7003 master
192.168.150.101 8001 slave
192.168.150.101 8002 slave
192.168.150.101 8003 slave

准备实例和配置

删除之前的7001、7002、7003这几个目录,重新创建出7001、7002、7003、8001、8002、8003目录:

# 进入/tmp目录
cd /tmp
# 删除旧的,避免配置干扰
rm -rf 7001 7002 7003
# 创建目录
mkdir 7001 7002 7003 8001 8002 8003

在/tmp下准备一个新的redis.conf文件,内容如下:

port 6379
# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称,不需要我们创建,由redis自己维护
cluster-config-file /tmp/6379/nodes.conf
# 节点心跳失败的超时时间
cluster-node-timeout 5000
# 持久化文件存放目录
dir /tmp/6379
# 绑定地址
bind 0.0.0.0
# 让redis后台运行
daemonize yes
# 注册的实例ip
replica-announce-ip 192.168.150.101
# 保护模式
protected-mode no
# 数据库数量
databases 1
# 日志
logfile /tmp/6379/run.log

将这个文件拷贝到每个目录下:

# 进入/tmp目录
cd /tmp
# 执行拷贝
echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf

修改每个目录下的redis.conf,将其中的6379修改为与所在目录一致:

# 进入/tmp目录
cd /tmp
# 修改配置文件
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf

4.3.启动

因为已经配置了后台启动模式,所以可以直接启动服务:

# 进入/tmp目录
cd /tmp
# 一键启动所有服务
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf

通过ps查看状态:

ps -ef | grep redis

发现服务都已经正常启动:

如果要关闭所有进程,可以执行命令:

ps -ef | grep redis | awk '{print $2}' | xargs kill

或者(推荐这种方式):

printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown

创建集群

虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。

我们需要执行命令来创建集群,在Redis5.0之前创建集群比较麻烦,5.0之后集群管理命令都集成到了redis-cli中。

1)Redis5.0之前

Redis5.0之前集群命令都是用redis安装包下的src/redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。

# 安装依赖
yum -y install zlib ruby rubygems
gem install redis

然后通过命令来管理集群:

# 进入redis的src目录
cd /tmp/redis-6.2.4/src
# 创建集群
./redis-trib.rb create --replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003

2)Redis5.0以后

我们使用的是Redis6.2.4版本,集群管理以及集成到了redis-cli中,格式如下:

redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003

命令说明:

  • redis-cli --cluster或者./redis-trib.rb:代表集群操作命令
  • create:代表是创建集群
  • --replicas 1或者--cluster-replicas 1 :指定集群中每个master的副本个数为1,此时节点总数 ÷ (replicas + 1) 得到的就是master的数量。因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master

运行后的样子:

这里输入yes,则集群开始创建:

通过命令可以查看集群状态:

redis-cli -p 7001 cluster nodes

测试

尝试连接7001节点,存储一个数据:

# 连接
redis-cli -p 7001
# 存储数据
set num 123
# 读取数据
get num
# 再次存储
set a 1

结果悲剧了:

集群操作时,需要给redis-cli加上-c参数才可以:

redis-cli -c -p 7001

这次可以了

散列插槽

我们存储的数据是面向多个master,而且master可能会宕机,所以不能让key值和master绑定,否则将来再次查找数据就可能找不到,所以这里的key并不是和master绑定而是和插槽绑定,让后把16384个插槽平分给所有的master。

Redis学习记录_第48张图片

当我查找数据的时候,会根据如图的规则来获取插槽,如果当前访问的节点没有该插槽,那么redis会帮我们定向到插槽所在的master节点,并且获取数据。
Redis学习记录_第49张图片

集群伸缩——动态添加删除节点

分为两步:节点的添加和插槽的分配。

主要使用的就是linux的命令,可以通过帮助文档查看使用就行。

1.节点的添加
分为两种添加方式:
1.默认添加为master节点,添加直接给定新的IP+端口然后告诉集群中的某一个已经存在的master就可以了

2.以slave节点的方式进行添加,需要在第一种方式后添加master的ID值
Redis学习记录_第50张图片

2.插槽的分配
Redis学习记录_第51张图片
这个命令执行后会向你请求接收插槽端口的ID,拷贝插槽端口的ID,你想拷贝那些插槽
Redis学习记录_第52张图片
Redis学习记录_第53张图片

手动故障转移

当一台master节点shutdown之后,会从他的分片集群内部再次选出一个master,但是有时候我们对于一些较老的节点,我们会希望更新节点,这时我们就需要手动故障转移,如下就是手动故障转移的命令和转移流程。
Redis学习记录_第54张图片

RedisTemplate访问分片集群

和哨兵的唯一差别就是从地址从指定master名称+每一个哨兵变为指定每一个节点。
Redis学习记录_第55张图片

单级缓存问题——多级缓存

上述的过程,仍然是先访问tomcat然后访问redis或者sql,存在如下两个问题

1.请求总是经过tomcat,tomcat会成为整个业务流程的瓶颈。

2.Redis缓存失效则会立刻对数据库产生冲击。

to be continue

你可能感兴趣的:(redis,学习,数据库)