Redis学习笔记

Redis

需要笔记的可以加群,这是我自己的群,一个月了就我一个人,希望能和一群人交流学习,无论你是高手还是糕手,到了这技术要学会,🐂也要吹上天

image-20211111220132185

1. Redis数据库相关指令

1.1 数据库操作指令

# 1.Redis中库说明
- 使用redis的默认配置器动redis服务后,默认会存在16个库,编号从0-15
- 可以使用select 库的编号 来选择一个redis的库

# 2.Redis中操作库的指令
- 清空当前的库  FLUSHDB
- 清空全部的库  FLUSHALL

# 3.redis客户端显示中文
-	./redis-cli  -p 7000 --raw

1.2 操作key相关指令

# 1.DEL指令
- 语法 :  DEL key [key ...] 
- 作用 :  删除给定的一个或多个key 。不存在的key 会被忽略。
- 可用版本: >= 1.0.0
- 返回值: 被删除key 的数量。 

# 2.EXISTS指令
- 语法:  EXISTS key
- 作用:  检查给定key 是否存在。
- 可用版本: >= 1.0.0
- 返回值: 若key 存在,返回1 ,否则返回0。

# 3.EXPIRE
- 语法:  EXPIRE key seconds
- 作用:  为给定key 设置生存时间,当key 过期时(生存时间为0 ),它会被自动删除。
- 可用版本: >= 1.0.0
- 时间复杂度: O(1)
- 返回值:设置成功返回1 。

# 4.KEYS
- 语法 :  KEYS pattern
- 作用 :  查找所有符合给定模式pattern 的key 。
- 语法:
	KEYS * 匹配数据库中所有key 。
	KEYS h?llo 匹配hello ,hallo 和hxllo 等。
	KEYS h*llo 匹配hllo 和heeeeello 等。
	KEYS h[ae]llo 匹配hello 和hallo ,但不匹配hillo 。特殊符号用 "\" 隔开
- 可用版本: >= 1.0.0
- 返回值: 符合给定模式的key 列表。

# 5.MOVE
- 语法 :  MOVE key db
- 作用 :  将当前数据库的key 移动到给定的数据库db 当中。
- 可用版本: >= 1.0.0
- 返回值: 移动成功返回1 ,失败则返回0 。

# 6.PEXPIRE
- 语法 :  PEXPIRE key milliseconds
- 作用 :  这个命令和EXPIRE 命令的作用类似,但是它以毫秒为单位设置key 的生存时间,而不像EXPIRE 命令那样,以秒为单位。
- 可用版本: >= 2.6.0
- 时间复杂度: O(1)
- 返回值:设置成功,返回1  key 不存在或设置失败,返回0

# 7.PEXPIREAT
- 语法 :  PEXPIREAT key milliseconds-timestamp
- 作用 :  这个命令和EXPIREAT 命令类似,但它以毫秒为单位设置key 的过期unix 时间戳,而不是像EXPIREAT那样,以秒为单位。
- 可用版本: >= 2.6.0
- 返回值:如果生存时间设置成功,返回1 。当key 不存在或没办法设置生存时间时,返回0 。(查看EXPIRE 命令获取更多信息)

# 8.TTL
- 语法 :   TTL key
- 作用 :   以秒为单位,返回给定key 的剩余生存时间(TTL, time to live)。
- 可用版本: >= 1.0.0
- 返回值:
	当key 不存在时,返回-2 。
	当key 存在但没有设置剩余生存时间时,返回-1 。
	否则,以秒为单位,返回key 的剩余生存时间。
- Note : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。

# 9.PTTL
- 语法 :  PTTL key
- 作用 :  这个命令类似于TTL 命令,但它以毫秒为单位返回key 的剩余生存时间,而不是像TTL 命令那样,以秒为单位。
- 可用版本: >= 2.6.0
- 返回值: 当key 不存在时,返回-2 。当key 存在但没有设置剩余生存时间时,返回-1 。
- 否则,以毫秒为单位,返回key 的剩余生存时间。
- 注意 : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。

# 10.RANDOMKEY
- 语法 :  RANDOMKEY
- 作用 :  从当前数据库中随机返回(不删除) 一个key 。
- 可用版本: >= 1.0.0
- 返回值:当数据库不为空时,返回一个key 。当数据库为空时,返回nil 。

# 11.RENAME
- 语法 :  RENAME key newkey
- 作用 :  将key 改名为newkey 。当key 和newkey 相同,或者key 不存在时,返回一个错误。当newkey 已经存在时,RENAME 命令将覆盖旧值。
- 可用版本: >= 1.0.0
- 返回值: 改名成功时提示OK ,失败时候返回一个错误。

# 12.TYPE
- 语法 :  TYPE key
- 作用 :  返回key 所储存的值的类型。
- 可用版本: >= 1.0.0
- 返回值:
	none (key 不存在)
	string (字符串)
	list (列表)
	set (集合)
	zset (有序集)
	hash (哈希表)

1.3 String类型

1. 内存存储模型

image-20211111154555963

2. 常用操作命令
命令说明
set设置一个key/value
get根据key获得对应的value
mset一次设置多个key value
mget一次获得多个key的value
getset获得原始key的值,同时设置新值
strlen获得对应key存储value的长度
append为对应key的value追加内容
getrange 索引0开始截取value的内容
setex设置一个key存活的有效期(秒)
psetex设置一个key存活的有效期(毫秒)
setnx存在不做任何操作,不存在添加
msetnx原子操作(只要有一个存在不做任何操作)可以同时设置多个key,只有有一个存在都不保存
decr进行数值类型的-1操作
decrby根据提供的数据进行减法操作
Incr进行数值类型的+1操作
incrby根据提供的数据进行加法操作
Incrbyfloat根据提供的数据加入浮点数

1.4 List类型

list 列表 相当于java中list 集合 特点 元素有序 且 可以重复

1.内存存储模型

image-20211111154621204

2.常用操作指令
命令说明
lpush将某个值加入到一个key列表头部
lpushx同lpush,但是必须要保证这个key存在
rpush将某个值加入到一个key列表末尾
rpushx同rpush,但是必须要保证这个key存在
lpop返回和移除列表左边的第一个元素
rpop返回和移除列表右边的第一个元素
lrange获取某一个下标区间内的元素
llen获取列表元素个数
lset设置某一个指定索引的值(索引必须存在)
lindex获取某一个指定索引位置的元素
lrem删除重复元素
ltrim保留列表中特定区间内的元素
linsert在某一个元素之前,之后插入新元素

1.5 Set类型

特点: Set类型 Set集合 元素无序 不可以重复

1.内存存储模型

image-20211111154639391

2.常用命令
命令说明
sadd为集合添加元素
smembers显示集合中所有元素 无序
scard返回集合中元素的个数
spop随机返回一个元素 并将元素在集合中删除
smove从一个集合中向另一个集合移动元素 必须是同一种类型
srem从集合中删除一个元素
sismember判断一个集合中是否含有这个元素
srandmember随机返回元素
sdiff去掉第一个集合中其它集合含有的相同元素
sinter求交集
sunion求和集

1.6 ZSet类型

特点: 可排序的set集合 排序 不可重复

ZSET 官方 可排序SET sortSet

1.内存模型

image-20211111154658014

2.常用命令
命令说明
zadd添加一个有序集合元素
zcard返回集合的元素个数
zrange 升序 zrevrange 降序返回一个范围内的元素
zrangebyscore按照分数查找一个范围内的元素
zrank返回排名
zrevrank倒序排名
zscore显示某一个元素的分数
zrem移除某一个元素
zincrby给某个特定元素加分

7.7 hash类型

特点: value 是一个map结构 存在key value key 无序的

1.内存模型

image-20211111154716102

2.常用命令
命令说明
hset设置一个key/value对
hget获得一个key对应的value
hgetall获得所有的key/value对
hdel删除某一个key/value对
hexists判断一个key是否存在
hkeys获得所有的key
hvals获得所有的value
hmset设置多个key/value
hmget获得多个key的value
hsetnx设置一个不存在的key的值
hincrby为value进行加法运算
hincrbyfloat为value加入浮点值

2.数据持久化

redis数据持久化分为两种:快照(rdb)、AOF(append only file)

当rdb和aof都开启时,redis在启动时会采用AOF来恢复数据

2.1 快照(RDB)

保存某一时刻的数据,redis默认使用的方式。它会生成.rdb后缀的文件(最后一次持久化的数据可能丢失)

生成快照文件的几种方式:

SAVE 和 BGSAVE 两个命令都会调用 rdbSave 函数,但它们调用的方式各有不同:

SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。
BGSAVE 则 fork 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。 Redis 服务器在BGSAVE 执行期间仍然可以继续处理客户端的请求。

Save是阻塞方式的;bgsave是非阻塞方式的。

  1. 客户端bgsave命令:调用fork创建一个子进程,子进程负责将快照写入到磁盘,主进程继续处理命令
  2. 客户端save命令:主进程负责快照生成,也就是说会导致redis无法对外服务,阻塞。
  3. 自动触发,当配置文件的条件满足时,触发快照生成:(执行bgsave命令)

image-20211111090713150

  1. 客户端shutdown命令:执行的是save命令

2.2 AOF(append only file)

将所有的写命命令记录到日志文件

要使用AOF,需开启,在配置文件中:

image-20211111092956254

日志追加频率: always、everysec、no

image-20211111093255232

always:来一个写命令就追加一次(以一个写命令为单位),虽然它可以保证数据不丢失,但是大量的写操作会极大降低redis运行效率。

everysec(推荐):以1s为单位,一秒追加一次,一秒内随便来多少个写命令,1s到了把1s内的写命令全部追加。可以看到这是redis默认使用的,redis可以保证,即使系统崩溃最多只会丢失1s的数据

no:不是不追加,而是把什么时候追加交给操作系统,操作系统需要的时候刷新即可

随着redis运行时间的增加,aof文件会越来越大,就会导致恢复数据越来越慢。所以为了压缩aof文件的大小,redis提供了AOF重写机制

2.3 AOF重写的两种方式

  1. 客户端命令:BGREWRITEAOF(bgrewriteaof)即后台重写,不会阻塞redis服务
  2. 配置自动触发

image-20211111101251176

这里有两个参数,解释下:

上面是一个百分比,这里是100%

下面是aof最小多少兆,这里是64

它两加起来就是这个意思:当aof文件达到64mb时,触发第一次重写,假如重写后为20mb;那么!下一次触发重写就是aof文件从20mb扩大了一倍(即100%的含义)为40mb时。你设置500%,下一次就是100mb触发重写

2.4 AOF重写原理

重写并不是在原有AOF文件的基础上进行压缩,而是将内存中的数据内容以命令的方式记录到一个临时的AOF文件中,然后将原来的替换掉,下面是我画的过程图:

image-20211111110444561

首先,我们应该弄明白怎么aof文件的体积就变小了?

举个例子,比如我对key为test的value(值为0)进行incr 1 ,加一操作,执行了1000次,aof中就记录了1000条记录,其实就只需要记录一条set test 1000不就是记录了test的值了吗。

它的过程我,描述一下:首先redis收到重写aof的命令时redis会调用fork()创建一个子进程,这个子进程会生成快照,然后将快照中的数据转变为写记录 写入临时的aof文件;与此同时,因为主线程还在工作,还会有新的写命令,主线程会将写命令继续追加到原来的文件(因为害怕子线程出现问题而导致后续的数据丢失),并将其缓存起来。当临时aof文件生成后,子进程会通知主进程,让主进程把缓存的写命令追加到临时文件。最后就将原来的文件替换掉,就完成了重写

3.SpringBoot 整合Redis

Spring Boot Data Redis提供了两个模板类:RedisTemplate和StringRedisTemplate。

RedisTemplate<Object,Object>,两个泛型都是Object类型,即key、value都是对象;但是Redis库中存储的都是String,所以RedisTemplate在存储时会自动将对象序列化,在取的时候会自动反序列化,这就要求使用的对象必须实现序列化接口(implements Serializable)!!!

而StringRedisTemplate<String,String>两个泛型都是String的,StringRedisTemplate是RedsiTemplate的子类

3.1 创建springboot项目

使用快速构建,你可以勾选上:lombok、web、redis

image-20211111170359494

这里也提供一下pom:

<dependencies>
    <!--redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!--单元测试-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3.2 编写配置

#redis地址
spring.redis.host=127.0.0.1
#redis端口号
spring.redis.port=6379
#使用哪个库
spring.redis.database=0

3.3 编写测试类(以StringRedisTemple为介绍)

我们需要注入StringRedisTemplate(它和RedisTemplate都交给了Spring容器管理)

@SpringBootTest
class RedisApplicationTests {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //StringRedisTemple测试类
    @Test
    void redis() {
        
    }

}

api介绍:

image-20211111171229582

测试方法:

@SpringBootTest
class RedisApplicationTests {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //StringRedisTemple测试类
    @Test
    void redis() {
        //往redis中存一个key为name,值为xiaoxiang的数据
        stringRedisTemplate.opsForValue().set("name","xiaoxiang");
        
        //取出name的值
        String value = stringRedisTemplate.opsForValue().get("name");
        System.out.println(value);
    }

}

结果:

image-20211111174054023

Redis库中也有该条数据:

image-20211111174318307

对key的操作:

//对key的操作:

//删除key
stringRedisTemplate.delete("name");
//设置key过期时间
stringRedisTemplate.expire("name", 10 , TimeUnit.DAYS);
//获取过期时间
stringRedisTemplate.getExpire("name");
//获取指定key的value是什么数据类型
stringRedisTemplate.type("name");

==对String类型的操作:==不能一一介绍,大家根据api字面意思对照前面的操作命令就能知道是干啥的了,可以自己玩一玩。其它类型的数据的使用和string差不多,api不同大家可自行体会就不介绍了

image-20211111175051311

 //hash相关操作 opsForHash
    @Test
    public void testHash(){
        stringRedisTemplate.opsForHash().put("maps","name","小黑");
        Object o = stringRedisTemplate.opsForHash().get("maps", "name");
        System.out.println(o);
    }
//zset相关操作 opsForZSet
@Test
public void testZSet(){
    stringRedisTemplate.opsForZSet().add("zsets","小黑",10);
    Set<String> zsets = stringRedisTemplate.opsForZSet().range("zsets", 0, -1);
    zsets.forEach(value-> System.out.println(value));
}

//set相关操作 opsForSet
@Test
public void testSet(){
    stringRedisTemplate.opsForSet().add("sets","xiaosan","xiaosi","xiaowu");
    Set<String> sets = stringRedisTemplate.opsForSet().members("sets");
    sets.forEach(value-> System.out.println(value));
}

//list相关的操作opsForList
@Test
public void testList(){
    // stringRedisTemplate.opsForList().leftPushAll("lists","张三","李四","王五");
    List<String> lists = stringRedisTemplate.opsForList().range("lists", 0, -1);
    lists.forEach(key -> System.out.println(key));
}


//String相关的操作 opsForValue
@Test
public void testString(){
    //stringRedisTemplate.opsForValue().set("166","好同学");
    String s = stringRedisTemplate.opsForValue().get("166");
    System.out.println(s);
    Long size = stringRedisTemplate.opsForValue().size("166");
    System.out.println(size);
}


//key相关的操作
@Test
public void test(){
    Set<String> keys = stringRedisTemplate.keys("*");//查看所有key
    Boolean name = stringRedisTemplate.hasKey("name");//判断某个key是否存在
    stringRedisTemplate.delete("age");//根据指定key删除
    stringRedisTemplate.rename("","");//修改key的名称
    stringRedisTemplate.expire("key",10, TimeUnit.HOURS);
  	//设置key超时时间 参数1:设置key名 参数2:时间 参数3:时间的单位
    stringRedisTemplate.move("",1);//移动key
}

如想了解更多api,请看视频:

操作key和String类型数据:https://www.bilibili.com/video/BV1jD4y1Q7tU?p=12

操作list、set、zset:https://www.bilibili.com/video/BV1jD4y1Q7tU?p=13

操作hash、RedisTemplate(重要):https://www.bilibili.com/video/BV1jD4y1Q7tU?p=14

绑定key API:https://www.bilibili.com/video/BV1jD4y1Q7tU?p=15

4.Redis应用场景

视频链接:https://www.bilibili.com/video/BV1jD4y1Q7tU?p=16

验证码存储(设置超时,时效性)、

具有时效性的业务(比如订单还有多少时间失,时效性)、

分布式集群中session共享、

利用zset类型(元素 分数),可排序,可以做排行榜之类的把商品id作为key,销量作为分数,选出排名即可。

分布式缓存

存储认证后的token(时效性)

解决分布式系统集群的分布式锁问题

5. 分布式缓存

现在主流是使用mybatis来操作数据库,但是mybatis的缓存(map做缓存)是存储在应用服务器本身也就是本地的,在分布式的系统中,是无法做到共享的。所以我们需要实现将mybatis的缓存放到redis中

5.1 环境构建

在这之前我们需要搭建好一个项目环境:需要使用到:数据库、mybatis、redis

image-20211112104810073

这是依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

编写配置:

#Redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0

#mysql
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/xpa?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=111111

#mybatis
mybatis.type-aliases-package=com.xp.redis1.entity
mybatis.mapper-locations=classpath:mapper/*.xml
#mybatis日志
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
image-20211112105312053

可以运行一下启动类,没问题就ok

当然,最好还是像写项目那样写个dao接口,在测试类中跑一下,比如我这样:就完全没问题了

image-20211112110248260

5.2 Mybatis缓存开启

现在我们在测试方法中,调用两次查询用户:

image-20211112113215390

日志:可以看出两次相同查询都是从数据库中拿的

image-20211112113305683

现在,我们开启mybatis的二级缓存,很简单:只需要在mapper的xml文件中加上cache标签:

image-20211112113738711

并且对这个使用到的对象序列化(这里是User):

image-20211112113900011

再运行:

image-20211112132815813

5.3 改造mybatis使用Redis做缓存

mybatis提供了一个接口来达到缓存,这个接口叫Cache,我们主要看下它的实现类:

image-20211112133247131

圈起来的就是mybatis默认使用的实现缓存的类了:PerpetualCache(永存)

image-20211112133522755

所以要想实现mybatis缓存使用Redis来做,我们就应该实现Cache接口,并重点关注这两个方法。

  1. 自定义类实现cache接口:

image-20211112135110349

public class RedisCache implements Cache {
    private final String id;
    public RedisCache(String id) {
        this.id = id;
        System.out.println("这个id是什么》》》》》》》》"+this.id);
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object key, Object value) {

    }

    @Override
    public Object getObject(Object key) {
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        return null;
    }

    @Override
    public void clear() {

    }

    @Override
    public int getSize() {
        return 0;
    }
}
  1. cache标签增加type属性:里面的值就是你自己实现Cache接口的类全路径名
<cache type="com.xp.redis1.cache.RedisCache"/>
  1. 试运行:

image-20211112135351080

image-20211112135417849

这个id就是mapper文件的namespace,即dao接口的全路径名。id就是缓存的唯一标识,你是谁的缓存

  1. 使用RedisTemplate

因为Cache的实现类是mybatis实例化的,并不是由Spring容器管理,所以我们是不能在实现类中注入它。所以我们首先要能拿到RedisTemplate。

自定义一个类实现ApplicationContextAware,获取到应用上下文便可以通过getBean方法,拿到RedisTemplate对象

@Configuration
public class ApplicationContextUtil implements ApplicationContextAware {

    private static ApplicationContext context;

    //获取SpringBoot应用上下文
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }
	//获取bean对象
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }
}
  1. 回到自定义缓存类,实现redis做缓存(使用redis的hash类型)
public class RedisCache implements Cache {

    private final String id;
    private RedisTemplate redisTemplate;

    public RedisCache(String id) {
        this.id = id;
        System.out.println("这个id是什么》》》》》》》》"+this.id);


        redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
        //将String类型的key使用String序列化(即存到redis中String类型的key不会变,其默认是JDK序列化会导致String类型也被序列化)
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //将Hash的String类型的key使用String序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    }

    @Override
    public String getId() {
        return id;
    }

    //将缓存输入保存到redis
    @Override
    public void putObject(Object key, Object value) {
        redisTemplate.opsForHash().put(id,key.toString(),value);
    }

    //从redis中取出缓存数据
    @Override
    public Object getObject(Object key) {
        return redisTemplate.opsForHash().get(id,key.toString());
    }

    @Override
    public Object removeObject(Object key) {
        return null;
    }

运行一下:

image-20211112143858868

打印一下两次的数据:

image-20211112144257536

redis中也有值:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y4KxPg2p-1636980438382)(…/AppData/Roaming/Typora/typora-user-images/image-20211112144941587.png)]

5.4 增删改时缓存清除问题

当我们对已缓存的数据做增删改的时候,需要清除缓存。做增删改时,mybatis是使用清除全部缓存,是使用的clear方法:

image-20211112160525798

我在dao接口中写了一个删除用户的方法,执行:

image-20211112160629694

可以看到是调用的clear方法,可以看下removeObject()方法的源码说明:

image-20211112160854886

所以我们只需要在clear方法中,删除redis中的key为id的数据即可:

@Override
public void clear() {
    redisTemplate.delete(id);
}

测试方法:

image-20211112162314381

此时,运行之后redis中应该是没有缓存数据的,运行看一下;确实被清空了

image-20211112162400932

当然redis做缓存只适用于增删改操作极少的数据,这里只是为了演示

完整的自定义缓存类:

public class RedisCache implements Cache {

    private final String id;
    private RedisTemplate redisTemplate;

    public RedisCache(String id) {
        this.id = id;
        System.out.println("这个id是什么》》》》》》》》"+this.id);


        redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
        //将String类型的key使用String序列化(即存到redis中String类型的key不会变,其默认是JDK序列化会导致String类型也被序列化)
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //将Hash的String类型的key使用String序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    }

    @Override
    public String getId() {
        return id;
    }

    //将缓存输入保存到redis
    @Override
    public void putObject(Object key, Object value) {
        redisTemplate.opsForHash().put(id,key.toString(),value);
    }

    //从redis中取出缓存数据
    @Override
    public Object getObject(Object key) {
        return redisTemplate.opsForHash().get(id,key.toString());
    }

    @Override
    public Object removeObject(Object key) {
        System.out.println("我执行力了");
        return null;
    }

    @Override
    public void clear() {
        redisTemplate.delete(id);
    }

    //返回缓存的数量
    @Override
    public int getSize() {
        return redisTemplate.opsForHash().size(id).intValue();
    }
}

5.5 这样使用缓存的问题

当我们的一个查询缓存数据和另一个查询缓存的数据没有关系时,这样做是没有问题的。

但是,如果存在关联数据,比如一个部门类中有员工列表,我们查询部门时的缓存数据中不仅仅包含部门也包含员工信息。此时某个员工离职,我们应该删除该员工,按照上面的清除缓存逻辑,我们只能将redis中key为员工dao的数据清除掉,但是此时部门的缓存数据中依然存在该离职员工,我们调用删除员工的方法并不能清除部门的缓存数据。

此时,我们需要将cache标签稍作改动:

比如我们将员工的mapper.xml改为:

<cache-ref namespace="cpm.xp.redis1.mapper.DeptMapper"/>

它的意思就是把员工的缓存放到部门的缓存中,两个缓存放一起,在redis中的key还是部门的namespace(被依赖的);这样无论是员工做了增删改还是部门做了增删改,都能将部门和员工的缓存一起清除。

5.6 优化key

上面的学习过程,可以看见存入redis的key或者hash的key都是乱七八糟的很长,所以我们可以对key进行优化?使用MD5

@Test
void aaa(){
    String key = "lsjhgourgh34567823532ds151.....67\5we";
    //使用md5
    String s = DigestUtils.md5DigestAsHex(key.getBytes());
    System.out.println(s);
}

image-20211112212416742

md5加密会生成32位的16进制字符串,并且加密内容不同生成的字符串一定不同(可用来比较两个文件是否相同),同一个加密内容的md5值始终不变

将这个方法应用到存缓存和取缓存的方法中即可

5.7 面试问题

1.什么是缓存击穿(穿透)?

缓存击穿是指:redis中不存在该条记录,数据库中也没有;但是大量的请求去查这个数据,导致redis中的缓存没用,全都涌向了数据库

如何解决缓存穿透?使用Mybatis的cache做缓存就已经解决了缓存击穿。mybatis在接收到缓存中没有、数据库中也不存在的查询结果,同样会做一个缓存(其value设置为null),这样就能避免缓存击穿了

2.什么是缓存雪崩?

在某一时刻,突然缓存全部失效,恰好此时来了大量请求,导致所有缓存无法利用,大量请求涌向数据库,导致数据库阻塞或者挂起

如何解决缓存雪崩?首先缓存全部失效并不是指redis挂了,在实际中往往会给缓存设置过期时间,导致缓存雪崩的情况就是所有缓存设置的失效时间相等(而且恰好是并发请求所有缓存都是在同一时刻存入redis),这样失效时间一到,缓存全部没了。避免的办法有两个:不设置过期时间(坏办法),每个模块缓存根据业务情况设置不同的有效期(好办法)

6.主从复制

主从复制的目的是实现主节点的数据备份,当主节点出现故障是不会让子节点来顶替主节点的(那是哨兵机制做的事)

image-20211113100506967

6.1 搭建redis主从复制

首先你需要准备至少三个redis服务,虚拟机也好,docker也行,又或者几台服务器。我这里就使用服务器演示了,

先安装redis,redis安装教程:https://blog.youkuaiyun.com/qq_42682745/article/details/120902149

因为我是用的服务器,所以需要三台都安装,

安装好之后,需要简单的配置主从节点,实现集群;两台子节点配置如下

image-20211113102932448

注:我是用的最新版本redis,老版本可能不叫replicaof,叫slaveof

ps:

image-20211113102317215

ok,两台都配置好之后,重启两个子节点(记得必须带配置文件启动哦)

我们看一下主节点有哪些数据:

image-20211113104147263

再去看看子节点:

image-20211113104218677

可以看见主节点的数据已经同步到子节点了,

下面我们在主节点中添加一个key:

image-20211113104413829

再去看看子节点:

image-20211113104444105

没毛病。

6.2 注意点

实现主从复制之后,子节点默认是只读的,不能做添加修改删除操作,比如:

image-20211113105305270

当然也可以开启字节点的写操作(极不推荐)在配置文件中还是在主从复制那块:

image-20211113105516267

7. 哨兵机制

主从复制时已经说明,主从复制只能做数据备份,要想实现主节点GG了子节点顶上来就需要使用哨兵机制。(自动故障转移,子节点(选一台)升级为主节点)

7.1 原理:

image-20211113110929993

在主从复制的基础上,哨兵会监视所有的节点,与所有的节点做心跳(哨兵向节点发送一个数据包,节点响应一个数据包给哨兵),当发现主节点心跳异常,此时子节点将停止复制主节点的数据,哨兵会从子节点中推选出一个新的主节点。选出新的主节点后,其余子节点将其作为主节点复制数据,等到原先的主节点恢复正常时,这个节点将作为新master的子节点,而不是恢复主节点的身份(特殊情况,如果没有新master产生,原来的主节点恢复后依旧是主节点)

哨兵服务应该起多个,一个会带来什么问题呢?(脑裂)

仅仅是因为哨兵的网络延迟,就很有可能导致推选新的master,而原来的master并没有问题,此时就会存在两个master。程序就不知道找哪个master

7.2 修改哨兵配置文件:

在redis安装目录下有个叫sentinel.conf的配置文件:

image-20211113130552912

vim sentinel.conf

哨兵默认端口是26379,非后台运行:

image-20211113130648762

修改配置,监听:

这是默认配置:

image-20211113131428195

解释一下:

sentinel monitor <master-name> <ip> <redis-port> <quorum>

sentinel monitor 是固定的,哨兵监控的意思

master-name:给主节点的名字(自己随便取)

ip:主节点的ip

redis-port:主节点redis的端口

quorum:当主节点宕机了,需要进行选举(投票),这个配置就是设置几个哨兵认为该主节点宕机了才会选出新的主节点,比如它默认是2,意思就是当有2个哨兵认为该主节点挂了,就会选出新的主节点。

下面就修改为我们自己的配置:(我也用两个哨兵来演示)

第一台哨兵:

image-20211113132349831

第二台哨兵:

两个哨兵配置应该相同(因为两个哨兵监控的同一个主从架构的主节点,所以哨兵配置相同)

7.3 启动哨兵

启动哨兵时,我们需要指定起配置文件在哪(绝对路径或者相对路径),为了直观我就用绝对路径

redis-sentinel /usr/local/redis6/sentinel.conf

image-20211113134929227

两个哨兵都启动之后,先看看主从复制正常不:

image-20211113135348693

清空数据之后,看看两个子节点:

image-20211113135457032

没有问题

7.4 模拟主节点挂了

现在到了激动人心的时刻,我们将主节点停掉:

image-20211113135611380

第一台哨兵:下午1:56发现主节点挂了

image-20211113135739492

第二台哨兵也发现了:

image-20211113140024852

这里好像出了点问题,有台服务器始终被哨兵认为挂掉了(具体原因没找到)

但也有新发现,此时重启主节点,重启后它依旧是master;也就是说在主节点挂了后,哨兵因为某些原因无法推选出新master,等到原来的主节点恢复后,在这种情况下原来的主节点依然是主节点。只是挂掉这期间就无法对外提供服务了。

还有问题:

image-20211113143235110

意思是:尝试故障转移到主机47.99.112.38 6739;原因是sentinel会根据监听情况不断修改sentinel.conf,会一直记录变化。是我之前的数据产生的影响,将其注释或者删掉即可(在配置文件末尾)

image-20211113150353398

然后重新启动

现在我把主节点挂掉:

image-20211113150600237

可以看到,哨兵推选39.108.83.106的子节点为主节点了,现在重启原来的主节点:

image-20211113150809804

发现它已经成为39的子节点了,

但是由于服务器之间的问题,结果终究是失败了

放到同一台服务器上是可以的,所以可以确定是服务器之间的问题,但是我没找到问题所在。。。。。。好难,头好晕

我腾讯的那台服务器直接:

image-20211115203445348

8. Redis集群

由于我没有选择上手练习了,所以,后续的笔记都是我复制的老师的笔记,希望你能结合视频看,不然有些东西写的不是很明白:https://www.bilibili.com/video/BV1jD4y1Q7tU?p=26

8.1 集群

Redis在3.0后开始支持Cluster(模式)模式,目前redis的集群支持节点的自动发现,支持slave-master选举和容错,支持在线分片(sharding shard )等特性。reshard

8.2 集群架构图

image-20211115202524097

8.3 集群细节

- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
- 节点的fail是通过集群中超过半数的节点检测失效时才生效. 
- 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
- redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value

image-20211115202611710

redis集群使用槽来存储值,集群的槽数量为16384(0~16383),将这些槽分配给redis节点。

存入值时, 使用CRC16算法计算key得到一个数值,比如999,那么这个key将会存储在上图中的Node1(0~5000)

8.4 集群搭建

判断一个是集群中的节点是否可用,是集群中的所用主节点选举过程,如果半数以上的节点认为当前节点挂掉,那么当前节点就是挂掉了,所以搭建redis集群时建议节点数最好为奇数,搭建集群至少需要三个主节点,三个从节点,至少需要6个节点

# 1.准备环境安装ruby以及redis集群依赖
//在线安装
- yum install -y ruby rubygems
//上传后安装
- gem install redis-xxx.gem

ruby是一个脚本

image-20211115202909312

# 2.在一台机器创建7个目录

image-20211115202936320

# 3.每个目录复制一份配置文件
[root@localhost ~]# cp redis-4.0.10/redis.conf 7000/
[root@localhost ~]# cp redis-4.0.10/redis.conf 7001/
[root@localhost ~]# cp redis-4.0.10/redis.conf 7002/
[root@localhost ~]# cp redis-4.0.10/redis.conf 7003/
[root@localhost ~]# cp redis-4.0.10/redis.conf 7004/
[root@localhost ~]# cp redis-4.0.10/redis.conf 7005/
[root@localhost ~]# cp redis-4.0.10/redis.conf 7006/

image-20211115202952769

# 4.修改不同目录配置文件
- port 	6379 .....                		 //修改端口
- bind  0.0.0.0                   		 //开启远程连接
- cluster-enabled  yes 	        			 //开启集群模式
- cluster-config-file  nodes-port.conf //集群节点配置文件
- cluster-node-timeout  5000      	   //集群节点超时时间
- appendonly  yes   		               //开启AOF持久化

# 5.指定不同目录配置文件启动七个节点
- [root@localhost bin]# ./redis-server  /root/7000/redis.conf
- [root@localhost bin]# ./redis-server  /root/7001/redis.conf
- [root@localhost bin]# ./redis-server  /root/7002/redis.conf
- [root@localhost bin]# ./redis-server  /root/7003/redis.conf
- [root@localhost bin]# ./redis-server  /root/7004/redis.conf
- [root@localhost bin]# ./redis-server  /root/7005/redis.conf
- [root@localhost bin]# ./redis-server  /root/7006/redis.conf

image-20211115203027844

# 6.查看进程
- [root@localhost bin]# ps aux|grep redis

image-20211115203042503

1.创建集群
# 1.复制集群操作脚本到bin目录中
- [root@localhost bin]# cp /root/redis-4.0.10/src/redis-trib.rb .

# 2.创建集群
- ./redis-trib.rb create --replicas 1 192.168.202.205:7000 192.168.202.205:7001 192.168.202.205:7002 192.168.202.205:7003 192.168.202.205:7004 192.168.202.205:7005

image-20211115203106493

# 3.集群创建成功出现如下提示

image-20211115203123236

2.查看集群状态
# 1.查看集群状态 check [原始集群中任意节点] [无]
- ./redis-trib.rb check 192.168.202.205:7000

# 2.集群节点状态说明
- 主节点 
	主节点存在hash slots,且主节点的hash slots 没有交叉
	主节点不能删除
	一个主节点可以有多个从节点
	主节点宕机时多个副本之间自动选举主节点

- 从节点
	从节点没有hash slots
	从节点可以删除
	从节点不负责数据的写,只负责数据的同步
3.添加主节点
# 1.添加主节点 add-node [新加入节点] [原始集群中任意节点]
- ./redis-trib.rb  add-node 192.168.1.158:7006  192.168.1.158:7005
- 注意:
	1.该节点必须以集群模式启动
	2.默认情况下该节点就是以master节点形式添加
4.添加从节点
# 1.添加从节点 add-node --slave [新加入节点] [集群中任意节点]
- ./redis-trib.rb  add-node --slave 192.168.1.158:7006 192.168.1.158:7000
- 注意:
	当添加副本节点时没有指定主节点,redis会随机给副本节点较少的主节点添加当前副本节点
	
# 2.为确定的master节点添加主节点 add-node --slave --master-id master节点id [新加入节点] [集群任意节点]
- ./redis-trib.rb  add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006  127.0.0.1:7000
5.删除副本节点
# 1.删除节点 del-node [集群中任意节点] [删除节点id]
- ./redis-trib.rb  del-node 127.0.0.1:7002 0ca3f102ecf0c888fc7a7ce43a13e9be9f6d3dd1
- 注意:
 1.被删除的节点必须是从节点或没有被分配hash slots的节点
6.集群在线分片
# 1.在线分片 reshard [集群中任意节点] [无]
- ./redis-trib.rb  reshard  192.168.1.158:7000

9.Redis实现分布式Session管理

9.1 管理机制

redis的session管理是利用spring提供的session管理解决方案,将一个应用session交给Redis存储,整个应用中所有session的请求都会去redis中获取对应的session数据。

image-20211115203159022

9.2 开发Session管理

1. 引入依赖
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
</dependency>
2. 开发Session管理配置类
@Configuration
@EnableRedisHttpSession
public class RedisSessionManager {
   
}
3.打包测试即可

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为了我的架构师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值