面对工作实践的最佳Redis教程

第一章  本地缓存与分布式缓存... 3

1.1本地缓存适合单体应用,适合在于一个jvm进程内使用... 3

1.2本地缓存在分布式情况下会存在本地缓存失效和本地缓存数据不一致的问题... 3

1.3我们在很多场景都会使用分布式缓存中间件... 3

1.4 在实际项目中我们是做多级缓存,如JVM级(本地缓存)+Redis缓存组成后端双级缓存... 3

第二章 缓存中间件redis在使用过程中存在的问题。... 4

2.1缓存穿透及解决方式... 4

2.2缓存雪崩及解决方式... 5

2.3缓存击穿及其解决方式... 5

2.4 缓存崩溃(失效)的解决方式... 11

第三章 Lua脚本与管道... 12

3.1 Lua. 12

3.2管道(Pipeline)... 13

第四章 缓存数据的一致性问题... 14

4.1(更新缓存的两种模式)缓存数据一致性问题的两种解决模式:... 14

4.2 Spring Cache (缓存层技术)... 16

第五章 redis重要命令info和Config. 18

5.1 info命令介绍... 18

5.2 config命令介绍(都有默认值)... 23

第六章 redis key的过期删除策略和内存淘汰机制... 25

6.1 定期删除及其问题:... 25

6.2 惰性删除及其问题 :... 26

6.3 redis内存淘汰机制... 26

第7章 redis Sentinel模式的搭建与redis集群模式的搭建... 28

7.1使用docker部署redis6.x Sentinel哨兵集群的步骤... 28

7.2 Docker 搭建redis6.x集群... 31

7.3使用Docker-compose安装搭建 redis6.x集群... 35

7.4 Springboot整合redis集群... 40

第八章 redis6.x的新特性... 41

6.1 ⽀持多线程... 41

6.2 引⼊了 ACL(Access Control List)权限控制... 42

6.3 客户端缓存... 44

第九章StringRedisTemplate与RedisTemplate的使用区别... 46

第十章 redis的性能优化... 51

10.1键值设计... 51

10.2命令使用... 55

10.3客户端使用... 55

Redis对于过期键有三种清除策略:... 60

第十一章 SpringCache的使用与多级缓存架构的实现... 61

11.1 SpringCache缓存框架介绍... 61

11.2 项⽬中引⼊SpringCache. 61

11.3 SpringCache框架常⽤注解Cacheable. 62

11.4 配置自定义的CacheManager和缓存过期时间... 64

11.5 ⾃定义缓存KeyGenerator 69

11.6 SpringCache框架常⽤注解CachePut 70

11.7 SpringCache框架常用注解CacheEvict 72

11.8 SpringCache框架@Caching. 73

11.9 SpringCache解决缓存击穿,雪崩,穿透问题的方式... 74

11.10 Spring Cache的不足:... 76

11.11 Ehcache+redis组成多级缓存架构的实现以及问题... 77

第十二章 redis持久化详解... 81

12.1 Redis持久化介绍... 81

12.2 RDB持久化介绍... 81

12.3 AOF持久化介绍... 84

12.4 AOF重写介绍... 85

12.5 AOF和RDB的选择问题... 87

12.6混合持久化:... 89

第十三章 RedisIO多路复用.. 91

 

第一章 本地缓存与分布式缓存

 

1.1本地缓存适合单体应用,适合在于一个jvm进程内使用

如把数据存到一个HashMap 中 HashMap中的数据是直接存在内存中的,我们可以把它当做一个本地缓存

当这个HashMap(本地缓存)中有数据时,直接去本地缓存中取数据,当本地缓存没数据时,再去数据库中查询,并把结果放一份放到本地缓存中

1.2本地缓存在分布式情况下会存在本地缓存失效和本地缓存数据不一致的问题

当一个应用,如商品服务,做集群后 如1,2,3服务,它们的本地缓存就是各自独立的(数据不共享),

如 第一次请求访问 1服务,去数据库中查询,并把结果放一份放到本地缓存中,第二次 请求访问的是2服务,2服务的本地缓存中无对应数据则又要去数据库中查询,造成缓存失效的效果。

此外如果123服务的本地缓存中都有数据,且1服务出现了更新缓存数据的操作,则1,2,3服务的缓存数据就不一致了

故本地缓存适合单体应用,适合在于一个jvm进程内使用

1.3我们在很多场景都会使用分布式缓存中间件

即分布式项目的每一个应用,共同使用缓存中间件:redis,mongoDb:好处:缓存中间件 是处于应用集群之上的缓存层,独立于一层。且可以将缓存中间件进行集群,做到高可用,高性能

我们可以将缓存中间件:redis做集群和分片存储(如1号redis存 0到1w数据2号redis存 1w到2w数据以此类推) 以提高性能。

1.4 在实际项目中我们是做多级缓存,如JVM级(本地缓存)+Redis缓存组成后端双级缓存

即请求到了后端之后先去访问JVM级缓存如果有数据直接返回没有数据再去redis中查,如果还没有,再去数据库中查,并把查到的数据存储到redis和本地缓存

第二章 缓存中间件redis在使用过程中存在的问题。

2.1缓存穿透及解决方式

指查询一个缓存和数据库中都不存在的数据,由于缓存没有命中,将去查询数据库但是数据库也无此条记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要数据库中去查询,失去了缓存的意义。

风险:可能有人利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃。

解决方法1把数据查询的null结果缓存(把NULL缓存到redis中下次从Redis中就可以取到对应结果了,虽然是空的数据),并给这个null结果加入短暂的缓存过期时间(过期后该NULL数据会从缓存中消失)(不然未来如果数据库中有了这个数据后,还是一直返回的缓存中NULL)。

解决方法2 使用分布式的布隆过滤器 对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

使用布隆过滤器好处是,当有黑客去请求大量不存在的缓存数据时,如几千万个Key,如果要用方法1,我们就要在Redis中缓存几千万个值为Null的Key大大增加消耗了Redis的内存,且请求这几千万个不存在的缓存数据往往是在很短时间内发出的,如果用方法1 ,就得向Redis中短时间发送几千万的命令,影响性能和并发

注意:我们仅仅把key放到布隆过滤器中,并没有把值也放到布隆过滤器中,故我们让布隆过滤器去存储几千万个Key时,消耗的内存要小于Redis几百倍

(一般就使用这个布隆过滤器去解决缓存穿透)

布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得 比较均匀。

向布隆过滤器中添加 key 时会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度 进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就 完成了 add 操作

向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位 置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个  key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组 比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。

这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少。

布隆过滤器的使用

可以用guvua包自带的布隆过滤器,

(但是实际上我们都用的是分布式的布隆过滤器(Redisson提供了分布式布隆过滤器),因为我们要共享集群中其他多个节点布隆过滤器中的Key,guvua包自带的布隆过滤器是JVM级别的在集群环境下无法共享,同步其他布隆过滤器中的Key)

1引入依赖:

1

2  com.google.guava

3  guava

4  22.0

5

2 初始化布隆过滤器并且向布隆过滤器中存Key

//1000:期望存入的数据个数,0.001:期望的误差率

BloomFilter bloomFilter =BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf‐8")), 1000, 0.001);

3 在缓存逻辑的上面加上一个布隆过滤器的过滤

 // 从布隆过滤器这一级缓存判断下key是否存在  Boolean exist = bloomFilter.mightContain(key);

 if(!exist){

 return ""; //没有就直接返回,不继续向后端执行了

 }

Redisson分布式布隆过滤器 的使用

17 RBloomFilter bloomFilter = redisson.getBloomFilter("phoneList");

18 //初始化布隆过滤器:预计元素为100000000L,误差率为3%

19 bloomFilter.tryInit(100000000L,0.03);

20 //将号码10086插入到布隆过滤器中

21 bloomFilter.add("10086");

22

23 //判断下面号码是否在布隆过滤器中

24 System.out.println(bloomFilter.contains("123456"));//false

25 System.out.println(bloomFilter.contains("10086"));//true

2.2缓存雪崩及解决方式

(缓存数据大面积(大量Key)同时失效):指我们设置缓存时key采用了相同的过期时间,导致全部缓存数据,或者大量缓存数据在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过大雪崩。

解决:在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期的重复率就会降低,就很难引发集体失效的事件。

2.3缓存击穿及其解决方式

(在高并发访问前,某一个高频访问的(key)缓存数据失效): 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被高并发访问,是一种“热点”的数据。如果这个key在大量请求同时进来前刚好失效,那么这一时刻所有对这个key的数据的查询就全部跑到Mysql中去了,我们成为缓存击穿,最终造成数据库压力过大导致崩溃。

解决:在查询数据库的方法中 加互斥锁(不可以使用共享锁),大量并发只让一个人(线程)去数据库中查询,其他人(线程)等待,第一个人查到以后把数据放一份到redis中再返回数据并释放锁,其他人获取到锁,先去缓存中查询,就会有数据,这样就不用去db中查。这样百万并发查询相当于只查了一次数据库--注意任何人得到锁以后,要先去redis中判断以下是否已经有缓存数据了,有就直接返回数据,没有再去数据库中查询或者设置热点数据永远不过期

/*在同一进程(同一应用节点)下,只要是所有线程抢占同一把互斥锁,就可以锁住需要这个锁的所有线程(当百万并发请求进来以后通过锁的竞争只会有一个线程竞争到锁,其他线程(不断的重试抢占锁)),当这个 synchronized (lock){}代码块结束以后锁就被打开了,下一个线程获取到锁继续执行代码以此类推 */

本地锁:synchronized 和juc(lock)

/*在同一个实例(节点)的Spring项目中 默认所有的组件都是单例的, 比如在这里,多个线程实际共享使用CategoryServiceImpl这个类的同一个对象故我们可以把当前CategoryServiceImpl对象当做锁 synchronized (this){} */

//但是对于集群中不同节点来说,每一个节点的单例组件都是不同的,故使用synchronized只能锁住当前进程(应用节点)下的线程,集群时,并不会有所有的线程都分配到同一应用节点中,

故本地锁无法控制大并发(在分布式情况下本地锁无法锁住所有进程的线程,本地锁仅可以锁住当前进程下的线程,性能较高),故要使用分布式锁(可以锁住所有进制的线程,但是性能相对低一些)(本地锁无法完全解决缓存击穿问题,但是它对缓存击穿问题的解决效果也是非常理想的,我们完全可以接收它的不足 )

注意事项:使用锁时,我们必须在锁打开之前(在 synchronized (lock){}这个方法体内)把查询到的结果放入redis缓存中(向redis缓存中插入数据需要时间),否则会出现--锁的时序问题 造成缓存数据不一致

2.3.1 Redis原生分布式锁的原理与使用

让一个服务集群中的所有节点的线程都去抢占同一个锁,抢占到锁的节点线程才可以执行业务逻辑(抢占到锁的节点可以去访问任何其他节点可以访问到的地方,如Mysql,redis),其他的节点线程以自旋(循环)的方式不断的重试抢占锁,暂时不能执行业务逻辑, 抢占到锁的节点线程执行完业务逻辑以后释放锁。

-为了保证集群中的一个方法在高并发情况下的同一时间最多只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题

分布式锁与本地锁的缺陷(锁的缺陷)

在大量写操作同时到来时,并发性能不高,只能串行化处理写请求(即 一个线程一个线程的执行写请求)--大量读操作的并发处理性能还行(在使用读写锁的情况下--不能用在解决缓存击穿上)

使用缓存实现分布式锁的演变(使用数据库也可以实现分布式锁,但是性能较低,可靠性较低)

阶段一 使用setNx("key",value)去模拟分布式锁 (在java中就是setIfAbsent 返回值时true和false)setNx--key不存在时,才可以设置这个key的值(返回ok(相当于抢占到锁),否则返回null (相当于没有抢占到锁)) 在并发时相当于在抢占锁,抢占成功就返回ok然后就可以执行业务逻辑,业务逻辑执行完就释放锁让其他线程继续抢占锁(即删除这个key即可),否则返回null(表示没有抢占到)暂时不能执行业务逻辑,但是一直处于重试状态,不断的重试抢占锁(可以设置休眠时间 ,让重试时间有一定的间隔)

问题:如果在释放锁前,业务代码出现了异常,或者断电,死机等问题,就会导致锁无法释放,然后造成死锁(某个锁永远无法释放,线程永远无法抢占到某个锁)。

解决方法:给锁设置过期时间(给这个key设置过期时间),即使锁没有及时释放,在一段时间后,锁就失效了

阶段二 在阶段一的基础上,在抢占到锁之后给锁设置了一个过期时间

问题:如果在竞争到锁之后,设置过期时间之前,出现断电,死机等问题,也会导致锁无法释放,然后造成死锁(某个锁永远无法释放,线程永远无法抢占到某个锁)。

解决方法:在抢占锁的同时设置过期时间,让抢占锁和设置过期时间同时进行,让他们成为一个原子性的命令,要么全部成功,要么全部失败。

阶段三 在阶段二的基础上在抢占锁的同时设置过期时间--redisTemplate.opsForValue().setIfAbsent("LocalLock", "111",300,TimeUnit.SECONDS); //第3个参数是时间数值,第4个是时间单位

问题:如果由于业务的执行时间很长,锁自己过期了,执行完业务后我们直接释放锁,有可能吧其他线程正在持有的锁给了释放了,导致更多的线程抢占到锁(多个线程都在同时执行代码)(即没锁住)。

解决方法:给自己抢占到的锁的值,加一个uuid唯一标识,在释放锁时,取出锁的值(key的值)进行判断---每个线程只能释放自己抢占到的锁,不可以释放别人抢占到的锁

阶段四 在阶段三的基础上给自己抢占到的锁,加一个uuid唯一标识,在释放锁时,取出锁的值(key的值)进行判断---每个线程只能释放自己的锁,不可以释放别人的锁

问题:在获取redis中锁的值(key的值)后,进行判断并释放锁之前,锁过期了,此时获取到的锁的值(key的值)已经不是当前锁的值了,但是会与当前线程的UUID相同,故会释放锁,此时释放的锁是其他线程的锁

解决方法:必须让判断锁是否是当前线程的锁,和释放锁两个操作同时进行并保持原子性。要么全部成功,要么全部失败。 使用原子操作:lua脚本+删除命令(即一个删锁的脚本)

阶段5 在阶段4的基础上 使用原子操作:lua脚本+删除命令(即一个删锁的脚本)来删除锁,这样第5个阶段就保证了加锁操作的原子性和解锁操作的原子性,从而保证锁住集群下同一应用所有节点的线程

if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

因为上述的锁,仅仅是实现了一个在分布式情况下自旋锁的功能,还有非常多其他锁的在分布式情况下的使用需要我们自己去封装,

问题:在这个阶段万一我们业务执行的时间很长超过了设置的锁的过期时间,此时虽然在前面的阶段4就解决了当前线程无法释放其他线程持有的锁的问题,但是此时当前节点的锁已经过期了,当前应用节点会出现一个线程它的业务还没执行完,其他线程就抢占到锁并执行业务的情况了,就没有锁住

阶段6:加一个后台线程每隔3分之1的缓存过期时间,就重新给当前线程持有的锁设置一次过期时间(该时间和以前一样)-即续命机制

注意:当释放锁时出现异常没有关系,锁设置了过期时间,最终还是会被释放的,我们要关注和担心的是锁不住的情况

阶段7 使用Redisson框架 Redisson也是一个redis客户端和lettuce,Jedis一样也是用来连接和操作Rediss的,不过它可以提供更加强大的功能特效,尤其在分布式架构下。如提供各种高级分布式锁和分布式对象等。

刚开始使用原生的Redisson,方便学习,使用它的Springboot启动器仅仅需要很少的配置

使用Redisson后 我们使用配置类中配置的RedissonClient去操作redis

2.3.2Redisson的基本原理与使用

1具体逻辑:Redisson内部去抢占锁和重入锁的时候使用的是Lua脚本逻辑去操作redis,抢占到锁后的看门狗机制(锁的续命记住)也是用的Lua脚本去操作Redis

没有抢占到锁的线程(在抢占锁的lua脚本逻辑中一个线程如果没有抢占到锁的时候会返回当前锁的过期时间),会使用while循环(自旋式)的继续尝试加锁,并不是每秒都在尝试加锁,而是会等到这把锁快过期的时候再去抢占一次锁

2使用redis去实现分布式锁的问题:(redisson实质也是使用redis去实现的分布式锁)

在redis中不管是哨兵架构,还是高可用集群架构,在主从切换过程中可能会出现,新的主节点还没有同步到老的主节点中的全部数据,如果这些还没同步的数据包括了我们的分布式锁,那么在当前线程业务还没执行完,其他线程在新的主节点去抢占分布式锁的时候就会抢占成功,那么这就导致了没锁住---但是我们一般是可以接受的

解决:使用强一致性的zookeeper去实现分布式锁(但是性能低)

Redisson提供的各种高级锁(原理基本就是上述的阶段6 以setNx("key",value)去模拟分布式锁 里面的锁基本都是实现与JUC包下的锁)

1 可重入锁(也是一种互斥锁--只能让一个线程抢占到锁)

Redisson:默认提供的锁是可重入锁,同一节点上的同一个线程如果获取了锁之后能够再次获取到同一个锁(锁尽量都是设计为可重入的,不可重入的锁容易导致死锁)

1 获取锁(只要锁名一致,就是同一把锁) (获取到锁,并不意味着加上锁了) redisson默认提供的锁是可重入锁

RLock mylock = redissonClient.getLock("mylock");

//2 加锁(竞争性的加锁)(使获取到的锁处于加锁状态)(如果当前线程没有抢占到锁就会阻塞式的等待,不会向下继续执行了)

mylock.lock();

或者boolean b = mylock.tryLock();

//有时候我们不希望一个线程等待太长的时间去抢占锁,指定一个时间如果在此期间没有抢占到锁,就直接返回系统繁忙下次再试—使用tryLock();去抢占锁

try {

//3 执行业务

System.out.println("竞争锁成功。。开始执行业务 。。。 当前线程号为"+Thread.currentThread().getId());

Thread.sleep(20000);

System.out.println("业务执行完成");

}catch (Exception e)

{

}

finally {

/*4 不管是否抛出异常都要解锁*/

mylock.unlock();

System.out.println("释放锁成功。。。。 当前线程号为"+Thread.currentThread().getId());

}

使用看门狗的原理解决死锁问题和混乱解锁问题(因为业务时间长,导致锁自动过期(其他线程就可以抢占到锁),执行完业务后释放锁,释放的却是其他线程的锁了,每个线程只能释放自己的锁,不可以释放别人的锁)

使用Redisson加的锁默认过期时间都是30秒(即看门狗时间) (我们自己指定默认过期时间通过修改Config.lockWatchdogTimeout来另行指定。)(我们也可以自己指定过期时间 mylock.lock(10,TimeUnit.SECONDS); 直接向redis执行Lua脚本,进行占锁。 但是这样看门狗就无法自动续期了)(使用默认过期时间时,向redis执行Lua脚本,进行占锁,并且会执行一个定时任务,定时的重新设置锁的过期时间为看门狗时间即30秒---这个定时任务 每 看门狗时间/3执行一次,即每10秒钟执行一次续期(重新设置过期时间) 并不是立马续期,或者每30秒续期一次,还有续期不是给当前过期时间加30秒,而是重新设置为30秒)

注意:1 我们尽量不要使用看门狗机制(即我们自己设置一个过期时间 mylock.lock(10,TimeUnit.SECONDS); )这个过期时间稍微设置大一点即可,超过业务的我们能接受的最大执行时间

因为看门狗机制(续期机制)十分消耗性能。

2 锁名要特别注意,一般在不同业务中,使用的锁名是不一样的,这样就可以减少锁的粒度,不让一个锁同时锁住多个业务或者多个毫无相关的业务数据(锁的粒度越小,速度越快,并发能力越强 ,我们要让锁的粒度越小越好)

3 缓存中的数据最好都要加过期时间,为了缓存一致性问题,以及死锁问题。

Redisson内部提供了一个监控锁的看门狗,它的作用是在RedissonClient实例被关闭前,不断的重新设置锁的有效期(不断的自动续期锁,默认自动重新设置锁的过期时间为30秒和看门狗时间相同 -注意并不是立马续期,或者每30秒续期一次,而是每 看门狗时间/3 s 续期一次),这样我们就不必担心业务时间长,导致锁自动过期的问题。

此外如果出现竞争到锁之后,突然死机断电的情况,这样会导致我们自己写的手动释放锁无法执行,从而造成死锁,看门狗解决了这个问题因为当服务器断电或者死机后,RedissonClient实例就会被关闭(这样看门狗也被关闭了),之后就不会继续自动续期锁了,当锁的过期时间到了之后,锁就自动释放了,从而不会出现死锁的情况

2 可重入公平锁(Fair Lock)---默认使用的锁都是不公平锁 (也是一种互斥锁)

多个线程不必抢占锁了,哪个线程的请求先到达,谁就先得到锁,之后按请求到达的顺序,依次获得锁,如果某个线程出现宕机时,Redisson会等待5秒后再继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒,其他概念与可重入锁一致

3 可重入读写锁 读锁(共享锁,多个线程可以同时抢占到这个锁,使这个锁出入加锁状态,可以看做没有加锁),写锁(排他锁)(互斥锁)(在并发情况下,只能有一个线程抢占到这个锁)其他概念和与可重入锁一致。如 看门狗的时间是30秒钟(默认超时 时间是30秒)

注意:1这里的读锁和写锁指的是,读写锁中的读锁和写锁,我们必须先获取读写锁再获取读写锁中的读锁和写锁

2 分布式可重入读写锁允许同时有多个读锁处于加锁状态或者一个写锁处于加锁状态

3 ,如果有一个写锁处于加锁状态(没释放),那么所有的读锁都无法进行加锁(处于等待状态) ,只要有一个读锁处于加锁状态(没释放),那么写锁也无法进行加锁(处于等待状态)

4 修改数据的业务 使用写锁,读取数据(查询数据)的业务 使用读锁

好处:在高并发情况下保证多个线程一定能读到最新数据,且可以保证数据的一致性

5 -读写锁即使一种共享锁,也是一种互斥锁,但是主要是使用的共享的部分(相当于没加锁)故不适用于解决缓存击穿问题

使用:

/*1 获取读写锁 ,读取数据的方法和写入数据的方法一般获取到的是同一个读写锁*/

RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ReadWriteLock");

/*2 获取读写锁中的写锁 */

RLock writeLock = readWriteLock.writeLock();

/*3 修改数据的业务 使用写锁*/

/* 使写锁处于加锁状态)*/

writeLock.lock();

/*4不管是否抛出异常都要解锁*/

writeLock.unlock();

/*1 获取读写锁,读取数据的方法和写入数据的方法一般获取到的是同一个读写锁*/

RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ReadWriteLock");

/*2 获取读写锁中的读锁 */

RLock readLock = readWriteLock.readLock();

/*3 读取数据(查询数据)的业务 使用读锁*/

/* 使读锁处于加锁状态)* /

readLock.lock();

/*4不管是否抛出异常都要解锁*/

readLock.unlock();

4 闭锁:当指定的其他线程的任务都执行完后,才能把闭锁打开(释放)再去 做某事,闭锁释放之前,任何调用了RCountDownLatch.await()这个方法的线程都处于阻塞状态,当闭锁打开后才可以正常通过

//1 获取闭锁,

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");

//2指定了几个其他线程的任务 --闭锁量

latch.trySetCount(1);

//3等待其他线程任务执行完成

latch.await();//将闭设为加锁状态

//4 执行我们自己的业务

另一个接口的任务

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");

任务执行完成,闭锁量-1

latch.countDown();

5 信号量(Semaphore)(比如用于库存的预热操作)(信号量的数量是原子性的,不会小于0)

(可以用来做分布式应用的限流操作:设置初始信号量为1W(在redis中设置)(代表某个接口,或者服务的最大处理量,),每个线程要调用业务逻辑,必须先尝试去获取信号量,再根据是否获取到信号量去判断是否可以执行业务,如果没获取到就直接返回当前系统忙,如果获取到了就正常执行业务,执行完业务,就释放一个信号量)

注意:如果是限流某个接口,就在某个接口上,做上述的操作,如果是限流一个完整的微服务,则在该微服务的每个接口上,都做上述操作。

RSemaphore semaphore = redisson.getSemaphore("semaphore");

//阻塞式的获取信号量,只有获取到信号,才可以继续往下执行

semaphore.acquire();

//尝试去获取信号量,不会阻塞,直接返回true或者falae,再根据是否获取到信号量去判断接下来执行什么任务 --一般用这个

boolean b=semaphore.tryAcquire();

RSemaphore semaphore = redissonClient.getSemaphore("semaphore");

//任务处理完-----释放一个信号量

semaphore.release();

2.3.3高并发分布式锁如何实现

当我们多个人要买不同的商品可以通过redis高可用集群架构的水平扩容来提高性能

但是如我们多个人要买一个商品

这个商品的数据之前在redis高可用集群架构中,只会放在一个主从节点群中,我们只能通过访问一个主从节点才能放问到它(扣库存业务我们使用该商品的id作为分步式锁的Key),水平扩容在此时是没有意义的

解决:使用分段锁

把该商品的库存数量分为10段,通过hash手段如 goods-1 goods-2..goods-10分别存储到

redis高可用集群架构的不同主从节点群中去,这样我们就可以通过10个主节点去访问到这个该商品的库存了(每个主节点中的存储的分布式锁是不一样的key,故可以支持10个线程同时访问,这样性能就提高了10倍,如果我们分100段就提高100倍)

2.4 缓存崩溃(失效)的解决方式

缓存崩溃指的是缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层。 由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并 发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降-redis可能没有挂但是并发量太大超过了redis的处理队列,处redis就会拒绝请求,然后大量请求还是会来到存储层), 于是大量请求都会达到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。 

预防和解决缓存崩溃问题, 可以从以下4个方面进行着手。

  1. 事前解决方案, 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。

即使一台Redis,或者几台redis,甚至整个机房的Redis都挂了以后Redis仍然可以提供使用

2) 事中解决方案:

依赖隔离组件为后端限流并降级。比如使用,限流降级组件

ehcache本地缓存应对零散的缓存中数据被清除掉的现象,另外一个主要预防缓存彻底崩溃,ehcache的缓存还能支撑一阵。

3) 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基 础上做一些预案设定。

4) 事后解决方案

Redis数据备份和恢复,快速缓存预热

第三章 Lua脚本与管道

3.1 Lua

Lua脚本语法简单,用的不多,看的懂就行(我们一般就是使用他来操作Redis,)

Redis Lua脚本

Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。

使用脚本的好处如下:

  1. 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器 上完成。使用脚本,减少了网络往返时延。这点跟管道类似。

(原来JAVA5次请求使用Redis命令才能完成的业务,现在使用Lua脚本编写,然后在Redis中一次性执行,只要使用一次Redis命令,大大减少了网络交互次数)

2、原子操作:Redis会将整个lua脚本作为一个整体执行中间不会被其他命令插入,这个lua脚本中的代码要么全部成功,要么全部失败管道不是原子的,不过 redis的批量操作命令(类似mset)是原子的。(最重要的特性)

3、替代redis的事务功能:redis自带的事务功能很鸡肋,报错不支持回滚,而redis的lua脚本几乎实现了 常规的事务功能,支持报错回滚操作,官方推荐如果要使用redis的事务功能可以用redis lua替代。

(当lua脚本报错时,会自动回滚lua脚本中对redis的DML操作)

从Redis2.6.0版本开始,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。

EVAL命令的格 式如下: 1 EVAL script numkeys key [key ...] arg [arg ...] 

前面的是键名参数可以在 Lua中通过全局变量KEYS数组,用1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。 在命令的后,那些不是键名参数的附加参数 arg [arg ...] ,可以在Lua中通过全局变量ARGV数组访问(也以1开始),

在 Lua 脚本中,可以使用redis.call()函数来执行Redis命令

使用StringRedisTemplate去执行Lua脚本

*测试lua脚本去扣减库存*/
@RequestMapping("/testlua")
public Object testlua()
{
String luascript="local count=redis.call('get',KEYS[1])"
+"local a=tonumber(count)"
+"local b=tonumber(ARGV[1])"
+"if a>=b then "
+" local c=count-b "
+" redis.call('set',KEYS[1],c)" +
" return c"
+" end " +
"return 0";

/*使用stringRedisTemplate去执行lua脚本,第二个参数为键名参数,都放在一个List中,在lua中通过KEYS[n]去访问,
第三个参数是省略号参数,表示额外参数在lua中通过ARGV[n]可以去访问--n>=1*/
Long result =stringRedisTemplate.execute(new DefaultRedisScript(luascript,Long.class), Arrays.asList("stock"),"10");
if(result!=0)
{
System.out.println("扣减库存成功,还剩:" + result);
return "扣减库存成功,还剩:" + result;
}
return "扣减库存失败,--库存不足";
}

注意:不要在Lua脚本中出现死循环和耗时的运算,(lua脚本里面的逻辑要尽量简单,不要搞什么批量操作,要让lua脚本执行的时间非常快)否则redis会阻塞,将不接受其他的命令,所以使用时要注意不能出现死循环、耗时的运算。redis是单进程、单线程执行脚本(必须执行完一条命令,或者整个lua脚本才可以开始执行下一个命令或者lua脚本)。管道不会阻塞redis。

3.2管道(Pipeline)

用来执行不同纬度的批量操作,并减少网络交互次数的。

1 原生命令:例如mget、mset。

Redis的原生批量操作命令都是同一纬度的(就是只能是同时批量取多个或者批量同时查多个)不可以批量同时又取又拿,是原子性的。

2 非原生命令:可以使用pipeline提高效率

管道是非Redis原生批量操作命令,可以批量执行不同纬度的操作,非原子性的

其实使用Lua脚本可能更好一些,它也可以批量执行不同纬度的Redis命令,此外还有事务功能,且是原子性的

管道:客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响 应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一 次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓 存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好,此外如果此时又其他客户端的命令进来也要等待管道中的命令都执行完才行,可能影响并发性能,故管道中的命令并不是越多越好,有几个就够了。 pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信 息;也就是pipeline并不是表达“所有command都一起成功”的语义(不是原子性的),管道中前面命令失败,后面命令 不会有影响,继续执行。

第四章 缓存数据的一致性问题

缓存数据一致性问题:如何保证缓存中的数据永远和数据库中的数据是一致的

4.1(更新缓存的两种模式)缓存数据一致性问题的两种解决模式:

1 双写模式:(修改了数据库中的数据后立即修改缓存中的数据):问题因为在高并发下延迟原因可能导致数据库中的数据暂时与缓存中的数据不一致(脏数据),等到缓存数据过期以后,就可以获取到最新的数据,这样读取最新数据有延迟,也可以加读写锁来解决这个问题(阻止并发写操作,一次只能写一个(把数据写入数据库并写入到缓存中)但是加锁后,并发写能力会很差)此外经常写的数据 ,实时一致性要求较高的数据,不应该放到缓存,就不应该放到缓存中

2 失效模式:修改了数据库中的数据后立即删除缓存中的数据,这样下次查询时,就会去数据库中查询最新的数据,并且放一份放到缓存中了

问题:也可能出现脏数据(即导致数据库中的数据暂时与缓存中的数据不一致)等到缓存数据过期以后,就可以获取到最新的数据。这样读取最新数据有延迟(也可以加读写锁 这样就不会出现脏数据,尽量不加)此外经常写的数据 实时一致性要求较高的数据,不应该放到缓存,就不应该放到缓存中

如下图:

故无论是失效模式还是双写模式,在并发下都可能导致缓存数据不一致问题,如果不做特殊处理他们只能保证缓存数据的最终一致性,无法保证缓存数据的实时一致性,如果要保证缓存数据的实时一致性可以加分布式锁去阻止并发写操作(对于读多写少的操作影响不大,并且我们要尽量减少锁的粒度)也可以加中间件canal

解决方法:1 加缓存过期时间 等到缓存数据过期以后,就可以获取到最新的数据,这样读取最新数据有延迟(即缓存一致性有延迟 出现脏数据)

2 在1的基础上加读写锁,不会出现脏数据 但是并发性能较差(对于经常读和经常写的数据)对于读多写少的数据影响较小

3 使用canal 缓存不一致问题,往往就是数据库的最后一次更新没有映射到Redis中

canal 相当于一个数据库的从库,数据库更新后,canal 会立马订阅去更新redis中的数据,这样以后在我们自己的应用中我们都可以不用管缓存的更新了,一旦数据数变化回自动帮我们更新缓存。。--坏处:又多了一个中间件影响吞吐量。

注意:一般缓存中的数据对实时性,一致性要求不高,并且读多写少,我们不必为了刻意追求一致性就去增加系统的复杂性和降低系统的性能。

4.2 Spring Cache (缓存层技术)

我们使用缓存时的逻辑:

(读模式)先从缓存中读取数据,如果没有再从慢速设备上读取实际数据(数据也会存入缓存)

(写模式)有两种 双写模式和失效模式

当我们有很多业务接口都要使用缓存时,我们总是要重复上述的缓存逻辑是十分麻烦的所以使用

Spring Cache (缓存层技术)可以帮我们解决这个问题。只需要很少的代码或者注解就可以完成多个接口的缓存逻辑,并且它统一了多种缓存技术(redis或者MongoDB等)(不管是什么缓存技术,都可以用Spring Cache 来实现)

4.2.1 Spring Cache的原理:

每一种缓存技术在SpringCache中都有对应的缓存管理器 CatheManager(是用来定义规则的,如缓存的过期时间等),每一种缓存管理器CatheManager下管理了多种不同名字的缓存组件(缓存组件是真正用来对缓存数据CRUD的。多个不同的缓存组件相当于数据的分区,是为了把各种缓存数据分开操作。如用户缓存组件,菜单缓存组件,)

4.2.2 Spring Cache的使用:

1 引入spring data Redis和Spring Cache的启动器依赖

2 在配置文件中进行配置

2.1配置Springcache具体使用的缓存技术的的类型 如redis

2.2也可以对缓存组件的名字进行配置,(但是这样Spring Cache就不会自动帮我们设置缓存组件名称了

2.3设置统一的缓存过期时间 以毫秒为单位。

2.4 Redis的配置也要提前配置好了(如Redis的地址等)

3 在启动类上添加 @EnableCaching 表示开启缓存功能

4 使用相关注解完成缓存操作

4.2.3 SpringCache的常用注解

@Cacheable 用来修饰一个查询数据(获取数据)的方法:表示,如果缓存中无对应数据则当前方法的返回值需要被缓存,如果缓存中有了对应缓存数据,就直接把该数据返回,无需再执行该方法去获取数据返回(这和缓存的读模式逻辑 是一样的 大大减少了代码量),

我们要指定把缓存数据放到哪个(或者多个)缓存组件中(按照业务名称) 缓存组件会自动帮我们向Redis中存数据 Key会自动生成 放到缓存组件名key(这个Key是一个数组类型的数据)下的::SimpleKey []中(自动生成的key),存的数据值是JDK序列化后的值(不是直接把值存进去) 默认缓存数据过期时间为-1(表示永不过期) 和向Redis中获取数据

如 @Cacheable({“category”})

注意:1 我们应该自己指定存到Redis中的Key名称(必做)(使用这个注解的key

属性)--取数据时也是根据我们自己指定的Key去取

如@Cacheable(cacheNames = {"Category"},key = "'LevelOneCategory'")

@Cacheable(cacheNames = {"Category"},key = "#root.method.name")

2 我们应该自己指定缓存过期时间(必做) 在配置文件中

3 存到Redis中的数据格式指定为JSON格式--提高可读性和兼容性(兼容其他语言)(必做)

进行一个自定义的RedisCacheConfiguration配置,设置Values的序列化格式

4 我们可以设置过滤条件,对这个方法返回值进行过滤后再存到Redis中

5 我们可以设置缓存条件,满足什么条件才可以把个方法返回值存到缓存中

@CacheEvict (失效模式的删除缓存中的数据)

把缓存中的数据删除

删除一个缓存组件(缓存分区)下指定的Key

如@CacheEvict(cacheNames = {"Category"},key="'xx'")

删除一个缓存组件(缓存分区)下所有的Key(用的多因为同一个缓存组件下的数据往往是相关的,同类型的,如都是分类数据,修改一个分类数据后我们应该把这个缓存组件下所有的Key都删除掉)

@CacheEvict(cacheNames = {"Category"},allEntries = true)

@CachePut (双写模式的更新缓存中的数据)

更新缓存,不影响方法的缓存

@Caching

用来复合缓存操作(组合上述3个注解的操作一起执行)

如 组合了多个@CacheEvict操作

@Caching(
evict = {
@CacheEvict(cacheNames = {"Category"},key = "'getLevelOneCategory'"),
@CacheEvict(cacheNames = {"Category"},key = "'Catalogjson'")
})

@CacheConfig 修饰一个类

在某一个类上共享缓存的配置

4.2.4 Spring Cache的不足:

虽然可以解决缓存穿透 (默认可以缓存空数据)和缓存雪崩问题(加了过期时间即可,因为实际上每一个数据的存储的时间节点是不一样的,就算是设置的过期时间相同,也很难会一起过期(即最终的过期时间点是分散的))

但是无法完全解决缓存击穿问题,并且内部没有使用读写锁去解决缓存一致性问题,只是使用了一个过期时间去解决缓存一致性问题(无法实时的达到缓存一致性)

解决:1 不使用Spring Cache,完全由我们自己去写缓存逻辑和锁逻辑

2 使用 Spring Cache,并开启synchronize(同步锁)(默认是无锁状态的),但是这个锁是个本地锁,无法完全解决缓存击穿问题,不过其实在并发量不是巨大的情况下(因为集群中的每台服务器也就最多发送一次查询数据库的请求 ),也是可以接受的 (这种方式最方便

@Cacheable(cacheNames = {"Category"},key = "#root.method.name",sync = true)

3 使用 Spring Cache与分布式锁与少量的自己写的缓存逻辑相结合使用:可以完全解决缓存击穿问题 (这种方式完全解决了问题
具体的实现逻辑
我们把获取数据的方法抽取成3个
1 总的获取数据的方法 里面调用使用分布式锁获取数据的方法(2 方法) 并加上@Cacheable注解
2 使用分布式锁获取数据的方法,里面写锁的逻辑和调用从数据库获取数据的方法
3 从数据库获取数据的方法(里面要加先判断是否缓存中有数据,有数据就直接返回。这是为了配合分布式锁,再后面因为锁的时序性,我们还有自己吧查到的数据保存到缓存中,注意自己设置的key要与springcache设置的key相同)

这样刚开始的并发请求,都可以去抢占锁(并且第一个抢占到锁的那个线程可以去数据库中查询 在方法3中,其他线程强占到锁后使用我们自己的逻辑,从缓存中获取数据 在方法3中),后面再进来的所有请求的都不会再调用总的获取数据的方法了(当然也不会去抢占锁,查询数据库了),直接返回缓存中的数据了

总结:对于常规数据(读多写少,实时性一致性要求不高的数据),完全可以使用Spring Cache

对于特殊数据还要求使用缓存的,我们就要特殊处理了如使用上述的3方法

第五章 redis重要命令info和Config

5.1 info命令介绍

进入客户端模式后可以输入该命令

可以查看服务器的各种信息和统计数值(由于项很多以下只有一些关键的信息项)

Server:有关redis服务器的常规信息redis_mode:standalone

# 运⾏模式,单机或者集群

multiplexing_api:epoll

# redis所使⽤的事件处理机制

run_id:3abd26c33dfd059e87a0279defc4c96c13962e de # redis服务器实例的随机标识符(⽤于sentinel和集群)

config_file:/usr/local/redis/conf/redis.conf # 配置⽂件路径

Clients:客户端连接部分

connected_clients:10 # 已连接客户端的数量(不包括通过slave连接的客户端)

Memory:内存消耗相关信息

used_memory:874152 # 使⽤内存,以字节(byte)

B为单位

used_memory_human:853.66K # 以⼈类可读的格式返回 Redis 分配的内存总量

used_memory_rss:2834432 # 系统给redis分配的内存即常驻内存,和top 、 ps 等命令的输出⼀致

used_memory_rss_human:2.70M # 以⼈类可读的格式返回系统redis分配的常驻内存top、ps等命令的输出⼀致

used_memory_peak:934040 # 内存使⽤的峰值⼤⼩

used_memory_peak_human:912.15K

total_system_memory:1039048704 # 操作系统的总内存 ,以字节(byte)为单位

total_system_memory_human:990.91M used_memory_lua:37888 # lua引擎使⽤的内存used_memory_lua_human:37.00K

maxmemory:0 # 最⼤内存的配置值,0是不限制maxmemory_human:0B maxmemory_policy:noeviction # 达到最⼤内存配

置值后的策略

Persistence:RDB和AOF相关信息rdb_bgsave_in_progress:0 # 标识rdb save是

否进⾏中

rdb_last_bgsave_status:ok # 上次的

save操作状态

rdb_last_bgsave_status:ok # 上次的

save操作状态

rdb_last_bgsave_time_sec:-1 # 上次

rdb save操作使⽤的时间(单位s) rdb_current_bgsave_time_sec:-1 # 如果rdb

save操作正在进⾏,则是所使⽤的时间

aof_enabled:1 # 是否开启

aof,默认没开启

aof_rewrite_in_progress:0 # 标识aof的

rewrite操作是否在进⾏中aof_last_rewrite_time_sec:-1 # 上次

rewrite操作使⽤的时间(单位s) aof_current_rewrite_time_sec:-1 # 如果

rewrite操作正在进⾏,则记录所使⽤的时间aof_last_bgrewrite_status:ok # 上次

rewrite操作的状态

aof_current_size:0 # aof当前

⼤⼩

Stats:⼀般统计

evicted_keys:0 # 因为内存

⼤⼩限制,⽽被驱逐出去的键的个数

Replication:主从同步信息(Replication中文是复制)

role:master # ⻆⾊

connected_slaves:1 # 连接的从库数

master_sync_in_progress:0 # 标识主

redis正在同步到从redis

CPU:CPU消耗统计

Cluster:集群部分

cluster_enabled:0 # 实例是否启⽤集群模式

Cluster:集群部分

cluster_enabled:0 # 实例是否启⽤集群模式

5.2 config命令介绍(都有默认值)

进入客户端模式后可以输入该命令

config命令可以动态地调整 Redis 服务器的配置(configuration)⽽

⽆须重启(使用的非常多该命令)

config get 配置名、(可以获取到当前该配置的值)

config set xxx (可以设置某个配置的值)如 config set daemonize yes

5.2.1 config set命令的注意事项

1 使用 CONFIG SET 需要注意的一点是, 并不是所有配置选项都可以在服务器运行时动态地设置的,有一些配置选项必须在服务器启动时才能设置。

如 设置数据库数量databases,端口号port ,是否开启多线程io-threads-do-reads

2 CONFIG SET 设置的选项值只会在服务器运行的过程中生效,一旦服务器关机,CONFIG SET 设置的选项值就会丢失。(CONFIG SET 设置的值,具体临时性)

举个例子, lua-time-limit 选项的默认值为 5000 ,虽然通过 CONFIG SET lua-time-limit 3000 可以将选项的值改为 3000 ,但这个修改只会在服务器的本次运行中有效,一旦服务器关闭并重启的话, luatime-limit 选项的值就会变回默认值 5000、

5.2.3 config rewrite 命令


如果服务器在启动时载入了配置文件,并且在服务器运行的过程中使用 CONFIG SET 修改了配置选项的值,那么执行 CONFIG REWRITE 命令可以将被修改的配置选项以及它的值永久写入到配置文件里面(这样以后启动后这个配置项的值就是之前修改的了)。(简单的说就是config set 就动态的临时修改一些配置,如果要这些配置下次启动时还能生效,那么就再执行一个 config rewrite )
举个例子,如果服务器启动时载入了包含以下内容的配置文件:
databases 32
lua-time-limit 5000
如果用户在服务器运行的过程中,执行了 CONFIG SET lua-time-limit 3000 命令,并且他打算将这一修改记录到配置文件里面的话,那么他可以执行 CONFIG REWRITE 命令,将配置文件的内容修改为:
databases 32
lua-time-limit 3000
这样服务器在下次启动并载入配置文件时,就会继续将 Lua 脚本的最大正常运行时间设置为 3000 毫秒。

第六章 redis key的过期删除策略和内存淘汰机制

背景

redis的key配置了过期时间,这个是怎么被删除的redis数据明明过期了,怎么还占⽤着内存? Redis 就只能⽤ 10G,你要是往⾥⾯写了 20G 的数据,会发⽣什么?淘汰哪些数据

redis key过期策略

定期删除+惰性删除

Redis如何淘汰过期的keys: set name xdclass 3600

6.1 定期删除及其问题:

隔⼀段时间,就随机抽取⼀些设置了过期时间的

key,检查其是否过期,如果过期就删除,

定期删除可能会导致很多过期 key 到了时间并没有被删除掉(因为是随机抽取一些设置了过期时间的Key,很可能一些已经过期的key还没有被删除),那咋整呢,所以就是惰性删除

6.2 惰性删除及其问题

概念:当⼀些客户端尝试访问过期的key时,key会被发现并被redis主动的删除(不会返回给客户端0

放任键过期不管,但是客户端每次从键空间中获取键时, 都检查取得的键是否过期,如果该键过期的话,就删除该键。但是还是可能有很多key已经过期,且没有客户端去访问(即两种策略均没有使用),导致常驻内存,浪费内存空间甚至耗尽,这就需要走内存淘汰机制了

Redis服务器对于过期的key实际使⽤的是惰性删除和定期删除两种策略: 通过配合使⽤这两种删除策略,服务器可以很好地在合理使⽤CPU时间和避免浪费内存空间之间取得平衡 -即在性能与功能之间取得了平衡。按照之间的假设,如果我们设置每秒钟去遍历所有的设置了过期时间的key,并检查每一个Key是否过期这样会导致浪费大量的cpu性能(遍历循环非常消耗计算性能)(且redis是单线程执行命令)这样的做法虽然可以极大的避免浪费内存控件但是导致redis性能急剧下降

惰性删除的问题

如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没⾛惰性删除,此时会怎么样?

如果⼤量过期 key 堆积在内存⾥,导致 redis 内存块耗尽了,就需要⾛内存淘汰机制

面试过程中可能问我们如果设计缓存中间件的内存淘汰机制:可以参考redis key的两种过期淘汰⽅式和内存淘汰机制

6.3 redis内存淘汰机制

背景

redis在占⽤的内存超过指定的maxmemory之后, 通过maxmemory_policy确定redis是否释放内存以及如何释放内存,redis提供多种策略策略

主要分为两大类一种是volatile(针对设置了过期时间的key进行淘汰)

另一种是allkeys针对所有的key继续淘汰

volatile-lru(least recently used)

最近最少使⽤算法,从设置了过期时间的键中选择空转时间最⻓的键值对清除掉;

volatile-lfu(least frequently used)

最近最不经常使⽤算法,从设置了过期时间的键中选择某段时间之内使⽤频次最⼩的键值对清除掉;

volatile-ttl

从设置了过期时间的键中选择过期时间最早的键值对进行清楚

volatile-random

从设置了过期时间的键中,随机选择键进⾏清除;

allkeys-lru

最近最少使⽤算法,从所有的键中选择空转时间最⻓的键值对清除;

allkeys-lfu

最近最不经常使⽤算法,从所有的键中选择某段时间之内使⽤频次最少的键值对清除;

allkeys-random

所有的键中,随机选择键进⾏删除;

noeviction (默认的)

(为了防止报错我们一定要设置为其他的算法)

不做任何的清理⼯作,在redis的内存超过限制之后,所有的写⼊操作都会返回错误(OOM);但是读操作都能正常的进⾏;

config命令动态设置配置文件时,遇到有 下划线_的key(配置项)需要用中横线-去替换下划线

第7章 redis Sentinel模式的搭建与redis集群模式的搭建

7.1使用docker部署redis6.x Sentinel哨兵集群的步骤

使用docker部署redis Sentinel哨兵集群的步骤肯定与直接放在linux上的redis部署哨兵集群的步骤和注意点不一样

如果我们关闭了机器(虚拟机),要完全重启Sentinel集群的话(即完全恢复到第一次搭建的情况)我们必须把最后一次选举出来的主节点的配置文件中的replicaof给加上并指定主节点的ip和端口(在467行的位置),因为之前故障转移重新选举出来的主节点会直接把之前的replicaof给去掉,此外还有其他从节点的replicaof要修改为当前的主节点的ip和端口。(如果之间挂的主机又连上去了肯定变成了从节点,此时它里面也会自动加上replicaof新的主节点IP端口,我们在重启的时候必须删掉即+#)并且把各个Sentinel的配置文件中,自动生成的部分给删掉并且修改里面的sentinel monitor mymaster,再按启动顺序: 主节点 Redis ➡ 从节点 Redis ➡ 启动Sentinel集群)重启 以上操作前提是上一次关闭前做了故障转移)

最好把之前的日志文件也删除掉

关机顺序最好是先关掉从,再关主

(如果之间挂掉的机器又重连了,他的配置文件和对应的sentinel也会被sentinel修改)我们可以不必做上述的操作直接可以实现哨兵---

  1. 先要把主从模式搭建好 (当前是多机器,多Docker模式下部署1主两从(3台机器,每台机器一个docker,每个机器上部署一个redis和sentinel),网络是手机热点)

  1. 添加哨兵配置文件到容器对应目录中(一个redis节点要对应一个Sentinel节点)

(放在容器的 /usr/local/redis/conf中)

#必须要先创建logfile对应的目录在容器中 mkdir /usr/local/redis/log否则,无法正常重启容器

port 26381

bind 0.0.0.0

daemonize yes

protected-mode no

pidfile "/var/run/redis-sentinel-1.pid"

logfile "/usr/local/redis/log/sentinel_26381.log"

#必须要先创建logfile对应的目录在容器中 mkdir /usr/local/redis/log否则,无法正常重启容器

dir "/tmp"

sentinel monitor mymaster 192.168.43.251 6380 2

sentinel down-after-milliseconds mymaster 5000

sentinel auth-pass mymaster 1234

sentinel failover-timeout mymaster 30000

sentinel announce-ip "192.168.43.47" #对外声明Sentinel的ip和端口

sentinel announce-port 26381

3 启动主从模式的redis容器(先启动主)

docker run -d -p 6382:6382 -p 26379:26379 -v/myredis6.0/data:/data --name myredis_6.2.1_salve2 daff6a1e55d9

docker run -d -p 6380:6380 -p 26380:26380 -v/myredis6.0/data:/data --name myredis_6.2.1_salve2 daff6a1e55d9

docker run -d -p 6381:6381 -p 26381:26381 -v/myredis6.0/data:/data --name myredis_6.2.1_salve2 daff6a1e55d9

4修改主从节点的redis.conf中的端口号和对外声明redis的ip和端口(因为有些端口号我们设置的不是6379,但是容器是先启动我们才能去改配置文件,可以去容器中修改,也可以直接去挂载目录中修改(宿主机挂载目录是随机生成的)) 还有announce-ip 和announce-port(对外声明redis的ip和端口)

5重启容器(先启动主节点)并开启Sentinel,

启动顺序: 主节点 Redis ➡ 从节点 Redis ➡ 启动Sentinel集群

启动哨兵的命令

/usr/local/redis/bin/redis-server /usr/local/redis/conf/sentinel-1.conf --sentinel

6 模拟主节点故障

通过命令让,主节点 睡眠 60s 来模拟宕机

docker exec -it e0c40096d826 /bin/bash -c '/usr/local/redis/bin/redis-cli -a 1234 -p 6382 DEBUG sleep 70'

注意:

1 不仅要暴露redis端口,也要暴露sentinel的哨兵的端口即容器启动时的端口映射要映射两个端口

2 为了让redis的哨兵能够发现和监测到从节点,我们一定要保证redis服务端口和容器的映射端口一致(即redis的端口设置为6380,那么映射的端口也一定相同为6380,不能为其他,否则redis哨兵启动后,从节点立马会被识别为sdown主观下线,那么Sentinel哨兵集群也没有意义了)

  1. 我们要修改redis配置文件中的announce-ip 和announce-port(对外声明redis的ip和端口)此外还要修改哨兵配置文件中的sentinel announce-ip,sentinel announce-port否则哨兵节点之间无法通信,哨兵节点与redis节点之间也无法通信(如果不加announce-ip 和announce-port)

4 如果我们关闭了机器(虚拟机),要重启Sentinel集群的话我们必须把最后一次选举出来的主节点的配置文件中的replicaof给加上并指定主节点的ip和端口,因为之前故障转移重新选举出来的主节点会直接把之前的replicaof给去掉,此外还有其他从节点的replicaof要修改为当前的主节点的ip和端口。并且把各个Sentinel的配置文件中,自动生成的部分给删掉并且修改里面的sentinel monitor mymaster,再按启动顺序: 主节点 Redis ➡ 从节点 Redis ➡ 启动Sentinel集群)重启--以上操作前提是上一次关闭前做了故障转移)

最好把之前的日志文件也删除掉

5 如果从节点、Sentinel节点被判定为主观下线,并不会进行后续的故障切换操作

可能出现的问题的参考文献

https://www.cnblogs.com/yq055783/p/14496449.html

当出现故障转移时(即投票选举主从切换时)会去修改其他从节点的redis配置文件的replicaof

重新指定主节点的ip和端口,重新选举出来的主节点则会直接把之前的replicaof给去掉并且也会在各个节点Sentinel的配置文件中自动生成内容并且会修改sentinel monitor mymaster这个选项 如果之间挂的主机后来又连上去了肯定变成了从节点,此时它里面也会自动加上replicao

7.1.1哨兵模式的优缺点

哨兵模式的优点

相比主从模式,可以实现⾃动主从切换(故障转移),可⽤性更⾼

哨兵模式的缺点

主从切换需要消耗一定的时间会丢失短暂数据(这时无法写和读)(在高并发期间是可能造成大量数据丢失,或者缓存击穿,导致数据库的崩溃)

主节点的写能⼒和存储能

(还是只有主节点可以写,存储能力还是处于单节点,因为主节点存的数据和其他节点一模一样,主节点存不下了,也就无法继续写了,存储就达到了上限了,数据不是分片存储的)且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率(重点

7.2 Docker 搭建redis6.x集群

注意点:

1 把之前的rdb、aof⽂件都删除,不然无法分配hash槽

rm -rf /myredis6.0/data/*

  1. 注意要在容器中创建一个该目录/usr/local/redis/log,否则启动容器会失败

mkdir /usr/local/redis/log

最少3主3从(3个主从节点群),组建3个主从节点群(3台虚拟机,3个docker,每个docker上部署一个主从节点群)

1 创建6个配置文件

bind 0.0.0.0

port 6386

#注意要在容器中创建一个该目录/usr/local/redis/log,否则启动容器会失败

logfile "/usr/local/redis/log/redis8.log"

dbfilename "redis8.rdb"

dir "/data"

appendonly yes

appendfilename "appendonly8.aof"

protected-mode no

masterauth "1234"

requirepass "1234"

#是否开启集群

cluster-enabled yes

cluster-config-file nodes-6386.conf

#节点连接超时时间

cluster-node-timeout 20000

#集群节点的ip,当前节点的ip(对外暴露的ip) #(不同机器要改)

cluster-announce-ip 192.168.43.189

#集群节点映射端⼝(应用程序连接的端口)(该集群节点对外暴露的端口)

cluster-announce-port 6386

#集群节点总线端⼝,节点之间互相通信,常规端⼝+1万该集群节点对外暴露节之间点通信的端口)

cluster-announce-bus-port 16386

2 启动容器 (要启动6个)

docker run -d -p 6383:6383 -p 16383:16383 -v/myredis6.0/data:/data --name myredis_6.2.1_cluster5 daff6a1e55d9

3 把配置文件复制到容器中redis的配置文件中(建议不要复制容器出现乱码)(或者直接修改里面的配置文件),并创建日志目录 mkdir /usr/local/redis/log

4 退出重启容器

(注意 启动后还没有分配HASH槽此时无法存储数据) 必须执行后面的加入集群命令

5 执行加⼊集群命令(其中⼀个节点执⾏即可)(对于一个集群执行一次即可)(除非我们是要重新搭建一个redis集群,不然执行一次该命令即可,重启集群后无需重复执行该命令了)

(执行该命令才开始分配槽位)(如果重启有问题就必须重新创建一个集群,必须删除之前数据文件rdb和aof,再重新执行该命令)

--cluster 构建集群全部节点信息

--cluster-replicas 1 主从节点的⽐例,1表示1主1从的

⽅式

/usr/local/redis6.00/bin/redis-cli -a 1234 --cluster create 192.168.43.47:6379 192.168.43.47:6380 192.168.43.251:6381 192.168.43.251:6382 192.168.43.189:6383 192.168.43.189:6384 --cluster-replicas 1

模拟主节点故障

通过命令让,主节点 睡眠 60s 来模拟宕机

docker exec -it e0c40096d826 /bin/bash -c '/usr/local/redis/bin/redis-cli -a 1234 -p 6382 DEBUG sleep 70'

注意:

1如果此时要以客户端模式访问,无需指定端口(当然也可以指定端口,最好指定主节点端口,因为丛节点只能读),因为我们要访问的是一个集群

-c 表示以集群方式启动

直 接 /usr/local/redis6.00/bin/redis-cli -c -a 1234

或者 /usr/local/redis6.00/bin/redis-cli -c -p 6386 -a 1234

(这里的-p 指端口,注意该端口的节点必须在该机器上)

进入客户端后可以输入以下查看集群信息和节点信息

# 集群信息

cluster info

#所有集群节点信息 (可以看出所有节点之间的主从关系)

cluster nodes

2检查状态某个节点的信息 (可以看出指定节点的具体信息)

/usr/local/redis6.00/bin/redis-cli -a 1234 --cluster check 192.168.43.47:6381

7.2.1向 Redis集群中添加(横向扩展)新的主从节点群

前提:Redis高可用集群已经启动了,(且要新增的这个redis实例必须已经启动)

步骤 1 新增2个redis实例在2个docker容器中

docker run -d -p 6385:6385 -p 16385:16385 -v/myredis6.0/data:/data --name myredis_6.2.1_cluster7 daff6a1e55d9

docker run -d -p 6386:6386 -p 16386:16386 -v/myredis6.0/data:/data --name myredis_6.2.1_cluster8 daff6a1e55d9

步骤 2 修改两个docker容器中里面redis的配置文件(内容如搭建中的配置文件一样)并创建日志目录 mkdir /usr/local/redis/log

步骤3 使用redis-cli --cluster add-node命令新增一个主节点8007(master),前面的ip:port为新增节点,后面的ip:port为已知存在节点(且该节点与新增主节点在同一机器上最好)(Redis集群中任意一个已经存在并且启动的节点),看到日志最后有"[OK]  New node added correctly"提示代表新节点加入成功

/usr/local/redis6.00/bin/redis-cli -a 1234 --cluster add-node 192.168.43.189:6385 192.168.43.251:6381

注意:1 当添加节点成功以后,新增的节点不会有任何数据,因为它还没有分配任何的slot(hash),我们需要为新主节点手工分配hash(如果不给这个新的主节点分配hash槽点那么,这个主节点永远的不会有数据存储进来,相当于形同虚设)

2 使用redis-cli --cluster add-node命令添加到集群的节点默认身份都是主节点,我们可以使用其他命令把它设置为某个主节点的从节点

步骤4

找到集群中的任意一个主节点,对其进行重新分配槽点工作(把这个主节点中的槽点移动一些到新添加的主节点中)。

/usr/local/redis6.00/bin/redis-cli -a 1234 --cluster reshard 192.168.43.251:6382

输出如下: ... ... How many slots do you want to move (from 1 to 16384)? 600 (

ps:需要多少个槽移动到新的节点上,自己设置,比如600个hash槽) What is the receiving node ID? 2728a594a0498e98e4b83a537e19f9a0a3790f38

(ps:把这600个hash槽移动到哪个节点上去,需要指定节点id-可以在打印信息中查看到节点id) Please enter all the source node IDs.  

 Type 'all' to use all the nodes as source nodes for the hash slots.   

Type 'done' once you entered all the source nodes IDs. Source node 1:all

(ps:输入all为从所有主节点(8001,8002,8003)中分别抽取相应的槽数指定到新节点中,抽取的总槽数为600个)  

... ... Do you want to proceed with the proposed reshard plan (yes/no)? yes (ps:输入yes确认开始执行分片任务)

步骤 5 为新加入到redis集群的主节点配置一个从节点-使用redis-cli --cluster add-node命令先添加一个从节点到集群中去

注意:(使用redis-cli --cluster add-node命令新增到Redis集群中的节点默认身份都是主节点,故我们要将它的身份改为8007这个主节点的从节点)

/usr/local/redis6.00/bin/redis-cli -a 1234 --cluster add-node 192.168.43.189:6386 192.168.43.189:6385

步骤 6 进入该6386从节点中将它的身份改为6385这个主节点的从节点

/usr/local/redis6.00/bin/redis-cli -c -p 6386 -a 1234

CLUSTER REPLICATE 78e416df46a86ac6191866(这是6385主节点的ID,可以在上面的打印信息中找到)

CLUSTER REPLICATE 节点ID:该命令表示重新配置一个节点成为指定mastersalve节点

7.3使用Docker-compose安装搭建 redis6.x集群

Docker-Compose构建redis集群(重点) 镜像名为redisclusterimage:v1

1 构建自定义的redis镜像 (已经构建在192.168.43.189机器中)镜像ID为 daff6a1e55d9

1.1(相比第三章3.8的redis_install.sh脚本 多了一行创建了一个日志目录mkdir –p /usr/local/redis/log )构建一个编译安装redis的脚本

#!/bin/bash

yum install -y gcc gcc-c++ make openssl openssl-devel

cd /home/redis-6.2.1

make && make PREFIX=/usr/local/redis install

mkdir -p /usr/local/redis/conf/

cp /home/redis-6.2.1/redis.conf /usr/local/redis/conf/

mkdir -p /data

mkdir –p /usr/local/redis/log

sed -i 's#127.0.0.1#0.0.0.0#' /usr/local/redis/conf/redis.conf

sed -i 's#protected-mode yes#protected-mode no#' /usr/local/redis/conf/redis.conf

sed -i '1230s#appendonly no#appendonly yes#' /usr/local/redis/conf/redis.conf

sed -i '444s#dir ./#dir ./data#' /usr/local/redis/conf/redis.conf

1.2 编写Dockerfile(相比第三章3.8构建redis镜像的Dockerfile 少了指定挂载目录 VOLUME ["/usr/local/redis/conf/"] #挂载配置文件的目录(无法指定宿主机的挂载目录-是随机生成的)(在192.168.43.189的/home目录中已经编写好)

FROM centos:7

ADD redis-6.2.1.tar.gz /home

COPY redis_install.sh /home

RUN sh /home/redis_install.sh

ENTRYPOINT /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis.conf

1.3 构建镜像:docker build -t redisclusterimage:v1

2 创建6个redis配置文件作为挂载文件(redis1.conf…redis6.conf)配置文件如下(不同的redis实例要修改部分配置,如各种端口,一些文件名 ip等)已经写在192.168.43.189的/redis_cluster目录中

bind 0.0.0.0

port 6379

requirepass "1234"

#注意要创建一个该目录/usr/local/redis/log,否则启动容器会失败

logfile "/usr/local/redis/log/redis1.log"

dbfilename "redis1.rdb"

dir "/data"

appendonly yes

appendfilename "appendonly1.aof"

protected-mode no

masterauth "1234"

#是否开启集群

cluster-enabled yes

cluster-config-file nodes-6379.conf

#节点连接超时时间

cluster-node-timeout 20000

#集群节点的ip,当前节点的ip(对外暴露的ip)

cluster-announce-ip 192.168.43.189

#集群节点映射端⼝(应用程序连接的端口)(该集群节点对外暴露的端口)

cluster-announce-port 6379

#集群节点总线端⼝,节点之间互相通信,常规端⼝+1万该集群节点对外暴露节之间点通信的端口)

cluster-announce-bus-port 16379

3构建docker-compose.yml文件 (在192.168.43.189机器的/tmp/RedisCluter_docker-compose中

version: '3'

services:

redis1:

image: ee98f212cc43

container_name: Docker_Compose_rediscluster1

ports:

- "6379:6379"

- "16379:16379"

volumes:

- /redis_cluster/redis1.conf:/usr/local/redis/conf/redis.conf

- /redis_cluster/log:/usr/local/redis/log

- /redis_cluster/data:/data

restart: always

redis2:

image: ee98f212cc43

container_name: Docker_Compose_rediscluster2

ports:

- "6380:6380"

- "16380:16380"

volumes:

- /redis_cluster/redis2.conf:/usr/local/redis/conf/redis.conf

- /redis_cluster/log:/usr/local/redis/log

- /redis_cluster/data:/data

restart: always

redis3:

image: ee98f212cc43

container_name: Docker_Compose_rediscluster3

ports:

- "6381:6381"

- "16381:16381"

volumes:

- /redis_cluster/redis3.conf:/usr/local/redis/conf/redis.conf

- /redis_cluster/log:/usr/local/redis/log

- /redis_cluster/data:/data

restart: always

redis4:

image: ee98f212cc43

container_name: Docker_Compose_rediscluster4

ports:

- "6382:6382"

- "16382:16382"

volumes:

- /redis_cluster/redis4.conf:/usr/local/redis/conf/redis.conf

- /redis_cluster/log:/usr/local/redis/log

- /redis_cluster/data:/data

restart: always

redis5:

image: ee98f212cc43

container_name: Docker_Compose_rediscluster5

ports:

- "6383:6383"

- "16383:16383"

volumes:

- /redis_cluster/redis5.conf:/usr/local/redis/conf/redis.conf

- /redis_cluster/log:/usr/local/redis/log

- /redis_cluster/data:/data

restart: always

redis6:

image: ee98f212cc43

container_name: Docker_Compose_rediscluster6

ports:

- "6384:6384"

- "16384:16384"

volumes:

- /redis_cluster/redis6.conf:/usr/local/redis/conf/redis.conf

- /redis_cluster/log:/usr/local/redis/log

- /redis_cluster/data:/data

restart: always

4 执行后台启动容器:docker-compose up –d (此时虽然启动了初始集群状态下所有redis实例,但是由于还没有分配hash槽所以无法存储数据)

5 输入加入集群的命令

/usr/local/redis6.00/bin/redis-cli -a 1234 --cluster create 192.168.43.189:6379 192.168.43.189:6380 192.168.43.189:6381 192.168.43.189:6382 192.168.43.189:6383 192.168.43.189:6384 --cluster-replicas 1

(其中⼀个节点执⾏即可)(对于一个集群执行一次即可)(除非我们是要重新搭建一个redis集群,不然执行一次该命令即可,重启集群后无需重复执行该命令了)

(执行该命令才开始分配槽位)(如果重启有问题就必须重新创建一个集群,必须删除之前数据文件rdbaof,再重新执行该命令)

--cluster 构建集群全部节点信息

--cluster-replicas 1 主从节点的⽐例,1表示1主1从的⽅式

模拟主节点故障

通过命令让,主节点 睡眠 60s 来模拟宕机

docker exec -it e0c40096d826 /bin/bash -c '/usr/local/redis/bin/redis-cli -a 1234 -p 6382 DEBUG sleep 70'

6进入客户端中检查集群信息

直 接 /usr/local/redis6.00/bin/redis-cli -c -a 1234

或者 /usr/local/redis6.00/bin/redis-cli -c -p 6386 -a 1234

(这里的-p 指端口,注意该端口的节点必须在该机器上)

进入客户端后可以输入以下查看集群信息和节点信息

# 集群信息

cluster info

#所有集群节点信息 (可以看出所有节点之间的主从关系)

cluster nodes

2检查状态某个节点的信息 (可以看出指定节点的具体信息)

/usr/local/redis6.00/bin/redis-cli -a 1234 --cluster check 192.168.43.47:6381

使用docker-compose构建redis集群的好处

1 构建速度相比直接用docker一个一个去构建快的多,一次性启动所有集群中所有的初始容器。因为我们的redis镜像和配置文件已经提前写好(就算要扩展节点也只要对配置文件做部分修改)(且对redis镜像进行了加强可以自动生成log目录)

Redis集群的优点

使⽤Redis的集群cluster主要为了解决单机Redis容量有限和主节点写能力有限的问题,redis集群将数据按⼀定的规则分配到多台机器(我们同时可以向多个主从节点群写数据),也存在一定的访问瞬断(但是相比Sentinel改善了非常多)(仅故障的主从节点群会有这种情况,其他节点群不会出现该情况,其他没有问题的主从节点群依然可以)(可扩展性强(在使用的过程中我们可以不断的往redis集群中添加主从节点群当然也可以删除),这样可用性就越高了)redis集群不需要sentinel也能完成节点移除和故障转移,redis集群里面会自动进行主从配置(和主从复制),无需我们自己去指定主从配置

为什么要使用Redis高可用集群架构

1如我们向Redis集群中存储一个key,这个Key会根据一定的运算规则存储Redis集群中某个主从节点群中,如果这个主从节点群的Master刚好挂了,这条命令也必须等待推选出新的Master才可以执行完(主从切换),不会存储到其他的主从节点群中去

--故也存在访问瞬断的问题

但是只是当前这个主从节点群会存在访问瞬断的问题,其他的主从节点群依然可以正常访问和操作,当我们Redis集群中的主从节点群特别多的时候,就会大大减少访问瞬断的情况的发生

它还具备高可用,高并发,分片存储的特性,故性能,可用性远高于哨兵架构

2 当数据量巨大时(一个节点的内存,存不下),我们可以做水平扩展主从节点群,当并发量巨大时(一个节点的承受的并发太高,我们也可以做水平扩展主从节点群,这样并发量就上来了

不在同个⽹络,所以集群改为阿⾥云公⽹ip地址才可以访问

公司开发部署都会⽤同个⽹络配置⽂件修改(公司内部一般都是用局域网IP从而减少带宽的影响)

#对外的ip

cluster-announce-ip 8.129.113.233 #对外端⼝

cluster-announce-port #集群桥接端⼝

cluster-announce-bus-por

动态修改redis的配置文件

先以客户端模式进入/usr/local/redis6.00/bin/redis-cli -p 6379 –a 1234

再输入 config set 要修改的配置项值

config set cluster-announce-ip 8.129.113.233

连接池添加 (之前添加)

org.apache.commons

commons-pool2

7.4 Springboot整合redis集群

cluster:

#命名的最多转发次数max-redirects: 3 nodes:

8.129.113.233:6381,8.129.113.233:6382,8.129.113

.233:6383,8.129.113.233:6384,8.129.113.233:6385

,8.129.113.233:6386

配置⽂件(注释Sentinel相关配置)

springboot整合redis集群时的问题

Redis高可用集群模式下master宕机,主从切换期间Lettuce连接Redis无法使用一直报错Redis command timed out的问题

联合运维和云厂商做了很多测试,发现凡是使用jedis客户端的服务都可以在15秒主从切换后恢复,而使用lettuce作为redis客户端的服务则无法恢复使用,一直抛超时的异常

解决办法(排除Lettuce客户端依赖使用jedis客户端依赖就可以解决这个问题)


org.springframework.boot
spring-boot-starter-data-redis


io.lettuce
lettuce-core




redis.clients
jedis

第八章 redis6.x的新特性

新版Redis6特性讲解

(redis所有版本-执行命令是单线程,所以我们有时候称它是单线程的)

8.1 ⽀持多线程

redis6 的多线程只是⽤来处理⽹络数据的读写和协议解析上(以前快多了),底层数据操作还是单线程

执⾏命令仍然是单线程,之所以这么设计是不想因为多线程⽽变得复杂,从而需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题

Redis6默认不开启多线程

io-threads-do-reads yes io-threads 线程数

io-threads-do-reads开启多线程这个配置项不支持使用config命令动态配置

官⽅建议 ( 线程数⼩于机器核数 )

4 核的机器建议设置为 2 或 3 个线程

8 核的建议设置为 4或6个线程,

开启多线程后,是否会存在线程并发安全问题?

不会有安全问题,Redis 的多线程部分只是⽤来处理⽹络数据的读写和协议解析,执⾏命令仍然是单线程 按顺序执⾏。

.Redis作者是如何点评 “多线程”这个新特性的?

关于多线程这个特性,在6.0 RC1时,Antirez曾做过说明:

Redis支持多线程有2种可行的方式:第一种就是像“memcached”那样,一个Redis实例开启多个线程,从而提升GET/SET等简单命令中每秒可以执行的操作。这涉及到I/O、命令解析等多线程处理,因此,我们将其称之为“I/O threading”。另一种就是允许在不同的线程中执行较耗时较慢的命令,以确保其它客户端不被阻塞,我们将这种线程模型称为“Slow commands threading”。

经过深思熟虑,Redis不会采用“I/O threading”,redis在运行时主要受制于网络和内存,所以提升redis性能主要是通过在多个redis实例,特别是redis集群。接下来我们主要会考虑改进两个方面:
1.Redis集群的多个实例通过编排能够合理地使用本地实例的磁盘,避免同时重写AOF。
2.提供一个Redis集群代理,便于用户在没有较好的集群协议客户端时抽象出一个集群。

补充说明一下,Redis和memcached一样是一个内存系统,但不同于Memcached。多线程是复杂的,必须考虑使用简单的数据模型,执行LPUSH的线程需要服务其他执行LPOP的线程。

我真正期望的实际是“slow operations threading”,在redis6或redis7中,将提供“key-level locking”,使得线程可以完全获得对键的控制以处理缓慢的操作。

8.2 引⼊ ACL(Access Control List)权限控制

之前的redis没有⽤户的概念,redis6引⼊了acl 可以给每个⽤户分配不同的权限来控制权限

通过限制对命令和密钥的访问来提⾼安全性,以使不受信任的客户端⽆法访问

提⾼操作安全性,以防⽌由于软件错误或⼈为错误⽽导致进程或⼈员访问 Redis,从⽽损坏数据或配置

⽂档:https://redis.io/topics/acl 常⽤命令

acl list 当前启⽤的 ACL 规则

(默认的话redis分配了一个默认用户default拥有全部的权限)

acl cat ⽀持的权限分类列表(大类,每个大类中还有很多小类)

acl cat hash 返回指定类别中的命令

acl setuser 创建和修改⽤户命令

acl deluser 删除⽤户命令

但是由于这个acl规则目前支持一个节点(单节点),只能对一个节点的用户进行限制权限,切换到另外一个节点后,acl规则就不生效了,acl目前不支持集群(所以用的少目前)

+ 将命令添加到⽤户可以调⽤的命令列表

中,如+@hash

- 将命令从⽤户可以调⽤的命令列表中移除

输入 命令ACL list 返回的信息是

"user default on #03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4 ~* &* +@all"

参 数

说明

user

⽤户

default

表示默认⽤户名,或则⾃⼰定义的⽤户名

on

表示是否启⽤该⽤户,默认为off(禁⽤)

#...

表示⽤户密码,nopass表示不需要密码

~*

表示可以访问的Key(正则匹配)

+@

表示⽤户的权限,“+”表示授权权限,有权限操 作或访问,“-”表示还是没有权限; @为权限分类,可以通过 ACL CAT 查询⽀持的分类。+@all 表示所有权限,nocommands 表示不给与任何命令的操作权限

8.3 客户端缓存

client side caching客户端缓存类似浏览器缓存⼀样

在服务器端更新了静态⽂件(如css、js、图⽚),能够在客户端得到及时的更新,但⼜不想让浏览器每次请求都从服务器端获取静态资源 类似前端的-Expires、Last-Modified、Etag缓存控制

面对工作实践的最佳Redis教程_第1张图片

第九章StringRedisTemplate与RedisTemplate的使用区别

9.1.RedisTemplate介绍

ValueOperations:简单K-V操作

SetOperationsset类型数据操作

ZSetOperationszset类型数据操作

HashOperations:针对map类型的数据操作

ListOperationslist类型的数据操作

9.2 RedisTemplate和StringRedisTemplate的区别

StringRedisTemplate继承RedisTemplate(更方便操作String类型的数据

默认情况下两者的数据是不共通的比如用StringRedisTemplate存储一个Keyredis中,我们用RedisTemplate去获取这个Key的时候返回的是一个Null反之亦然因为默认的序列化机制不同导致实际存储或者获取的key,如果我们把RedisTemplate存储Key的序列化机制重新定义为String类型的话,就可以数据互通了

StringRedisTemplate默认采String的序列化策略(我们在redis中看这个key或者值,是字符串的格式,可视化比较好

RedisTemplate默认采的是JDK的序列化策略,存储数据到Redis中时会先将数据(key-value)序列化成字节数组然后在存Redis数据库(故实际存储到redis中的Key名与我们指定的不一样)(我们在redis中看这个Key或者值基本就是乱码)(即采用RedisTemplate去存储一个对象到redis中时必须实现Serializable

总结

redis数据库⾥⾯本来操作的是字符串数据的时候,那使StringRedisTemplate即可

数据是复杂的对象类型,那么使RedisTemplate是更好的选择

StringRedisTemplate存储的数据在redis中可视化更好,但是性能会下降,RedisTemplate存储的数据在redis中可视化较差基本就是乱码,但是性能会提高(默认情况下:即采用RedisTemplate去存储一个对象到redis中时必须实现Serializable⼝,但是StringRedisTemplate去存储一个对象到Redis中只需把对象转为JSON字符串即可

操作

String结构

存储字符串

存储对象

9.3 序列化的详解

StringRedisTemplateRedisTemplate操作同一个key为啥获取不到值,核就是序列化机制导致key

什么是序列化我们只有把一个对象数据序列化它才可以在网络中进行传输)

把对象转换为字节序列的过程称为对象的序列化

把字节序列恢复为对象的过程称为对象的反序列化

对象的序列化主要有两种

把对象的字节序列永久地保存到硬盘上,通常存放在件中

络上传送对象的字节序列。

Redis为什么要序列化

性能可以提,不同的序列化式性能不

可视化具更好查看

默认的jdk式会乱码(POJO类需要实现

Serializable)(即采用RedisTemplate去存储一个对象到redis中时必须实现Serializable

JSON式则不,且可视化具更好查

9.4 自定义RedisTemplate的序列化方式

因为RedisTemplate序列化方式的可视化查看非常糟糕,但是它又更加方便存储复杂类型的数据,所以我们会去定义RedisTemplate序列化不针对StringRedisTemplate,让它的可视化效果变得更好

定义RedisTemplate序列化式,提供了多种可选择策略

1 JdkSerializationRedisSerializer

POJO对象的存取场景,使JDK本身序列化机制

默认机制 ObjectInputStream/ObjectOutputStream 列化操作

2   StringRedisSerializer(字符串类型的序列化)

Key或者value为字符串

3 Jackson2JsonRedisSerializer

jackson-json具,将pojo实例序列化成json格式存储

GenericFastJsonRedisSerializer

javabeanjson之间的转换,同时也需要指定Class类型

...

重新定义RedisTemplate的 Key的序列化机制为字符串类型序列后采用RedisTemplate去存储一个对象到redis中时,该对象就不比实现Serializable接口了,此时StringRedisTemplate与RedisTemplate存储的数据就可以互通了

自定义序列化方式的代码如下(以后直接复制即可)

@Configuration
public class RedisTemplateConfiguration {

   
/*重新配置RedisTemplate的序列化机制*/
    /*重新定义RedisTemplate的 Key的序列化机制为字符串类型序列后采用RedisTemplate去存储一个对象到redis中时,该对象就不比实现Serializable接口了*/   
@Bean
   
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        RedisTemplate redisTemplate =
new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

       
// 使用Jackson2JsonRedisSerialize 替换默认序列化(创建Jackson2JsonRedisSerialize序列化器)
       
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

       
//对Jackson2JsonRedisSerialize序列化器进行一些设置
        /* jackson 对象转JSON的设置*/
       
ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.
ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

       
// 设置key和value的序列化规则
       
redisTemplate.setKeySerializer(new StringRedisSerializer());

       
//StringRedisTemplate的序列化就是key和值都设置为字符串类型序列化
        //redisTemplate.setValueSerializer(new StringRedisSerializer());
       
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

       
// 设置hashKey和hashValue的序列化规则
       
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

       
// 设置支持事物
        //redisTemplate.setEnableTransactionSupport(true);
       
redisTemplate.afterPropertiesSet();
       
return redisTemplate;
    }
}

第十章 redis的性能优化

10.1键值设计

10.1.1 key名设计

    1. 【建议】: 可读性和可管理性

以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id

1 trade:order:1

    1. 【建议】:简洁性

保证语义的前提下,控制key的长度,当key长度较多时,内存占用也不容忽视,例如:

1 user:{uid}:friends:messages:{mid} 简化为 u:{uid}:fr:m:{mid}

    1. 【强制】不要包含特殊字符

反例:包含空格、换行、单双引号以及其他转义字符

10.2.2 value设计

【强制】:拒绝bigkey(防止网卡流量、慢查询)如果一个Key对应的VALUE太大那么,在Redis中对这个Key进行操作会非常慢(比如查询),这样就阻塞了其他Redis命令的执行,在高并发时会影响并发量

Redis中,一个字符串最大512MB,一个二级数据结构(例如hashlistsetzset)可以存 储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况,我就会认为它是bigkey

Bigkey的两种情况

  1. 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey(比如一个json字符串中放了N多数据)
  2. 非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多一般来说,string类型控制在10KB以内,hashlistsetzset元素个数不要超过5000个元素 反例:一个包含200万个元素的list

非字符串的bigkey,不要使用del删除使用hscansscanzscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞)

Bigkey的危害:

如果一个Key对应的VALUE太大那么,在Redis中对这个Key进行操作会非常慢,这样就阻塞了其他Redis命令的执行,在高并发时页会影响并发量)

  1. 导致redis阻塞
  2. 网络拥塞(高并发不仅仅是程序服务器或者硬件的问题,还可能是网路的问题,带宽不够也会非常影响并发性能)

bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey1MB,客户端每秒访问量 1000(访问这个大Key),那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾(现在的话每秒就最多可以承受128的并发了,而redis单机最高有10W大大浪费了Redis的性能),而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey 能会对其他实例也造成影响,其后果不堪设想。

  1. 过期删除

有个bigkey,它安分守己(只执行简单的命令,例如hgetlpopzscore等),但它设置了过 期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy- expire yes),就会存在阻塞Redis的可能性(bigkey删除时间过长尤其对于非字符串的bigkey)

bigkey的产生:

一般来说,bigkey的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个 例子:

  1. 社交类:粉丝列表,如果某些明星或者大v不精心设计下,必是bigkey
  2. 统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是bigkey
  3. 缓存类:将数据从数据库load出来序列化放到Redis里,这个方式非常常用,但有两个地方需 要注意,第一,是不是有必要把所有字段都缓存;第二,有没有相关关联的数据,有的同学为了 图方便把相关数据都存一个key下,产生bigkey

Bigkey的优化

1

big list list1list2...listN

big hash:可以讲数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成200key,每个key下面存放5000个用户数据

  1. 如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理。

2.1【推荐】:选择适合的数据类型。

例如:实体类型(要合理控制和使用数据结构,但也要注意节省内存和性能之间的平衡) 反例:

  1. set user:1:name tom
  2. set user:1:age 19
  3. set user:1:favor football

1 hmset user:1 name tom age 19 favor football

正例 :

2.2【推荐】:控制key的生命周期,redis不是垃圾桶。

建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期)

10.2命令使用

1【推荐】 O(N)命令关注N的数量

例如hgetalllrangesmemberszrangesinter等并非不能使用,但是需要明确N的值。有遍 历的需求可以使用hscansscanzscan代替。

  1. 【推荐】:禁用命令

禁止线上使用keysflushallflushdb等,通过redisrename机制禁掉命令(给这些命令改一个名字让别人不知道去用),或者使用scan的方式渐进式处理。

  1. 【推荐】合理使用select(最好是不同的系统使用不同的Redis实例)

redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还

是单线程处理,会有干扰。

    1. 原生命令:例如mgetmset
    2.    非原生命令:可以使用pipeline提高效率。
    【推荐】使用批量操作提高效率

但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)

注意两者不同:

  1. 1. 原生是原子操作,pipeline是非原子操作。
  2. 2. pipeline可以打包不同的命令,原生做不到
  3. 3. pipeline需要客户端和服务端同时支持

  1. 【建议】Redis事务功能较弱,不建议过多使用,可以用lua替代

10.3客户端使用

  1. 【推荐】

避免多个应用使用一个Redis实例(避免多个微服务使用同一个Redis实例)

正例:不相干的业务拆分,公共数据做服务化

  1. 【推荐】

使用带有连接池的数据库,可以有效控制连接,同时提高效率,标准使用方式:

  1. JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
  2. jedisPoolConfig.setMaxTotal(5);
  3. jedisPoolConfig.setMaxIdle(2);
  4. jedisPoolConfig.setTestOnBorrow(true);

5

6 JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null);

7

  1. Jedis jedis = null;
  2. try {
  3. jedis = jedisPool.getResource();
  4. //具体的命令
  5. jedis.executeCommand()
  6. } catch (Exception e) {
  7. logger.error("op key {} error: " + e.getMessage(), key, e);
  8. } finally {
  9. //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。

  1. if (jedis != null)
  2. jedis.close();

19 }

序号

参数名

含义

默认值

使用建议

1

maxTotal

资源池中最大连接数

8

设置建议见下面

2

maxIdle

资源池允许最大空闲

的连接数

8

设置建议见下面

3

minIdle

资源池确保最少空闲

的连接数

0

设置建议见下面

4

blockWhenExhauste d

当资源池用尽后,调用者是否要等待。只有当为true时,下面 的maxWaitMillis才会

生效

true

建议使用默认值

5

maxWaitMillis

当资源池连接用尽 后,调用者的最大等

待时间(单位为毫秒)

-1:表示永不超时

不建议使用默认值

6

testOnBorrow

向资源池借用连接时是否做连接有效性检测(ping),无效连接

会被移除

false

业务量很大时候建议设置为false(多一次ping的开销)。

7

testOnReturn

向资源池归还连接时是否做连接有效性检测(ping),无效连接

会被移除

false

业务量很大时候建议设置为false(多一次ping的开销)。

8

jmxEnabled

是否开启jmx监控,可

用于监控

true

建议开启,但应用本

身也要开启

连接池参数含义:

优化

建议:

  1. maxTotal最大连接数(连接池中连接的最大数量,并不是一开始连接池中的连接数量就会直接到最大值),早期的版本叫maxActive 实际上这个是一个很难回答的问题,考虑的因素比较多:

业务希望Redis并发量客户端执行命令时间

Redis资源:例如 nodes(例如应用个数) * maxTotal 是不能超redis的最大连接数maxclientsRedis服务器默认的最大的客户端连接数是10000)。

资源开销:例如虽然希望控制空闲连接(连接池此刻可马上使用的连接),但是不希望因 为连接池的频繁释放创建连接造成不必靠开销。

以一个例子说明假设:

一次命令时间borrow|return resource + Jedis执行命令(含网络) )的平均耗时约为1ms,一个连接的QPS大约是1000

业务期望的QPS50000(根据业务所需的并发去预估设置,设连接池的最大连接数)

但这个值不是越大越好,一方面连接太多占用客户端和服务端资源,另一方面对于Redis这种高


那么理论上需要的资源池大小是 50000 / 1000 = 50 个。但事实上这是个理论值,还要考虑到要比理论值预留一些资源,通常来讲 maxTotal 可以比理论值大一些。

QPS的服务器,一个大命令的阻塞即使设置再大资源池仍然会无济于事。

2 maxIdle和minIdle

maxIdle实际上才是业务需要的最大连接数,maxTotal是为了给出余量,所以maxIdle不要设置过 小,否则会有new Jedis(新连接)开销。

连接池的最佳性能是maxTotal = maxIdle,这样就避免连接池伸缩带来的性能干扰。但是如果并发量不大或者maxTotal设置过高,会导致不必要的连接资源浪费。一般推荐maxIdle可以设置为 按上面的业务期望QPS计算出来的理论连接数,maxTotal可以再放大一倍。

minIdle(最小空闲连接数),与其说是最小空闲连接数,不如说是"至少需要保持的空闲连接数 ",在使用连接的过程中,如果连接数超过了minIdle,那么继续建立连接(web应用启动后,Redis连接池中的初始连接数量默认是0(其他关系数据的连接池的初始连接数量默认是最小连接数),当我们要使用redis命令是就会new 一个连接,连接数小于等于maxldle的连接会一直保持存在,大于maxldle的数量连接在执行完业务后会被连接池慢慢释放掉。),如果超过了maxIdle,当超过的连接执行完业务后会慢慢被移出连接池释放掉。

如果系统启动完马上就会有很多的请求过来那么可以给redis连接池做预热,比如快速的创建一些redis连接,执行简单命令,类似ping(),快速的将连接池里的空闲连接提升到minIdle的数

量。

连接池预热示例代码:

1 List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());

2

  1. for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
  2. Jedis jedis = null;
  3. try {
  4. jedis = pool.getResource();
  5. minIdleJedisList.add(jedis);
  6. jedis.ping();
  7. } catch (Exception e) {
  8. logger.error(e.getMessage(), e);
  9. } finally {
  10.   //注意,这里不能马上close将连接还回连接池,否则最后连接池里只会建立1个连接。。
  11. //jedis.close();

14   }

15 }

  1. //统一将预热的连接还回连接池
  2. for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
  3. Jedis jedis = null;
  4. try {
  5. jedis = minIdleJedisList.get(i);
  6. //将连接归还回连接池
  7. jedis.close();
  8. } catch (Exception e) {
  9. logger.error(e.getMessage(), e);
  10. } finally {

26   }

27 }

总之,要根据实际系统的QPS和调用redis客户端的规模整体评估每个节点所使用的连接池大小。

  1. 【建议】

高并发下建议客户端添加熔断功能(例如netflix  hystrix)

  1. 【推荐】

设置合理的密码,如有必要可以使用SSL加密访问

  1. 【建议】

Redis对于过期键有三种清除策略

被动删除当读/写一个已经过期的key,会触发惰性删除策略,直接删除掉这个过 key

主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰 一批已过期的key(问题:很多热过期的数据无法被及时删除,与惰性删除互相弥补),但是还是可能出现很多冷数据无法被及时删除导致已用内存超过maxmemory这时就需要内存淘汰机制了

当前已用内存超过maxmemory限定时,触发主动清理(内存淘汰机制)策略

操作”del key”同步到从结点。

REDIS 运行在主从模式时,只有主结点才会执行被动和主动这两种过期删除策略,然后把删除

第三种策略的情况如下:

当前已用内存超过maxmemory限定时,会触发主动清理策略

根据自身业务类型,选好maxmemory-policy(最大内存淘汰策略),设置好过期时间。如果不设置 最大内存, Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。(默认没有设置内存淘汰机制策略,我们最好设置上去)

默认策略是volatile-lru,即超过最大内存后,在过期键中使用lru算法进行key的剔除,保证不过期 数据不被删除,但是可能会出现OOM问题。

其他策略如下:

allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间 为止。

allkeys-random:随机删除所有键,直到腾出足够空间为止。volatile-random: 随机删除过期键,直到腾出足够空间为止。

volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。

noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。

第十一章 SpringCache的使用与多级缓存架构的实现

11.1 SpringCache缓存框架介绍

SpringCache简介

档:https://spring.io/guides/gs/caching/

Spring 3.1起,提供了类似于@Transactional注解事务的注解Cache持,且提供了Cache抽象

提供基本的Cache抽象,便切换各种底层Cache 只需要更少的代码就可以完成业务数据的缓存。(方法的返回值会被SpringCache缓存,当下次调用该方法时,如SpringCache中有对应的缓存则不会执行该方法而是直接返回数据)

提供事务回滚时也动回滚缓存

较复杂的缓存逻辑⼼⼀个是Cache,缓存操作的API个是CacheManager管理各类缓存,有多个缓存框架的实现。

11.2 项⽬中引⼊SpringCache

org.springframework.boot

spring-boot-starter- cache

 

配置件指定缓存类型

spring: cache:

type: redis #

 

启动类开启缓存注解

@EnableCaching

 

11.3 SpringCache框架常注解Cacheable

Cacheable注解(用于缓存数据)(一般都是标注在Service层实现类上)

Cacheable注解类标记在法上,也可以标记在个类上

缓存标注方法的返回结果,标注在法上缓存该法的返回值,标注在类上则缓存该类所有的法返回值  注意该注解 使用后 一旦缓存了方法该次调用对应的数据(要根据Key来判断是否缓存了该次调用的数据),则不会触发真实法的调

Cacheable注解中的几个重要的参数

1 value 缓存名称,可以有多个 (在SpringCache生成的Key前会加上value::,故最终的Key为value::key)但是我们一旦在配置文件中指定了key-prefix: 这个value就失效了

#如果设置了Value则真正缓存的Key的名前必有 value::

key 缓存的key规则,可以springEL表达式,默认是法参数组合

condition 缓存条件,使springEL编写,返回true才会去缓存数据

4 cacheManager 缓存管理器,可以通过这个参数来选择不同的SpringCache的缓存管理器(可以不使用该参数,从而使用默认的cacheManage

spEL表达式

#methodName 当前被调法名

root.methodname

#args 当前被调法的参数列表

root.args[0]

#result 法执后的返回值

result

Cacheable(value={"SpringCache"},key="methodName+'_'+#size+'_'+#page")
public Object testSpringCache(int size,int page)

{
}

//这里缓存后的key的名为SpringCache:: testSpringCache_size_page

SpringCache:: testSpringCache_10_5

#如果设置了Value则真正缓存的Key的名前必有 value::

11.4 配置自定义的CacheManager和缓存过期时间

我们直接使用SpringCache缓存数据到Redis中,默认情况下会出现一些问题,比如默认没有设置缓存过期的时间,导致缓存数据与真实数据不一致,缓存数据存储到redis中时用可视化工具查看显示乱码(因为SpringCache缓存值的默认的序列化方式是JDK

解决方式1:我们在配置文件中指定SpringCache缓存数据到Redis中的过期时间(所有SpringCache缓存数据的过期时间是统一的),之后创建一个配置类中,在配置类中修改一下SpringCache在缓存数据时Value值的序列化方式为JSON字符串类型。
 
   
这种方式的主要问题:所有SpringCache缓存数据的过期时间是统一的,无法定制
 
   
cache:

  type: redis

  redis:

    time-to-live: 30000 #指定SpringCache 缓存到redis中数据的过期时间为多少--以毫秒为单位
   * */
 
   
具体的配置类的代码如下

EnableConfigurationProperties(CacheProperties.class)

@Configuration

public class MyCacheConfig {



     @Autowired

     private   CacheProperties CacheProperties;



     /*自定义的Spring Cache的Redis缓存管理器的配置

     * 目的是为了让缓存到Redis中的键值在可视化工具中不出现乱码(改了一下value的序列化方式为JSON字符串类型)

     * */

  @Bean

  public RedisCacheConfiguration getRedisCacheConfiguration()

  {

      RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig();

      cacheConfig=  cacheConfig.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));



      /*存到Redis中的数据格式指定为JSON格式*/

      cacheConfig=  cacheConfig.serializeValuesWith((RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer())));



      /*并且让配置文件中的配置也一起生效,否则配置文件中的内容就失效了*/

      org.springframework.boot.autoconfigure.cache.CacheProperties.Redis redisProperties = CacheProperties.getRedis();



      if(redisProperties.getTimeToLive()!=null)

      {

      cacheConfig=cacheConfig.entryTtl(redisProperties.getTimeToLive());

     }

      if(redisProperties.getKeyPrefix()!=null)

      {

          cacheConfig=cacheConfig.prefixKeysWith(redisProperties.getKeyPrefix());

      }

      if(!redisProperties.isCacheNullValues())

      {

          cacheConfig=cacheConfig.disableCachingNullValues();

      }

      if(!redisProperties.isUseKeyPrefix())

      {

          cacheConfig=cacheConfig.disableKeyPrefix();

      }



      return   cacheConfig;

  }





}
 
   
 
   
 
   

                                     

解决方式2:在配置类中修改redis缓存数据值的序列化器和配置多个RedisCacheManager

来设置多种过期时间(不同的RedisCacheManager配置的过期时间不同)(并指定默认的RedisCacheManager 从而有默认的过期时间),用户可以通过选择不同的RedisCacheManager(我们配置每一个RedisCacheManager的方法名不同,选择指定方法名,就可以指定要用的RedisCacheManager)来达到让SpringCache缓存指定的数据时,有指定的过期时间


配置类代码如下

@Configuration

@EnableConfigurationProperties(CacheProperties.class)
public class AppConfiguration {

//如果要能够成功注入CacheProperties则该类必须加上@EnableConfigurationProperties(CacheProperties.class)注解
@Autowired
private   CacheProperties CacheProperties;


    /*设置多个CacheManager的原因是,我们可以指定不同的CacheManager来达到,SpringCache缓存指定的数据时有指定的过期时间

    /**

     * 1分钟过期

     *

     * @param connectionFactory

     * @return

     */

    @Bean

    public RedisCacheManager cacheManager1Minute(RedisConnectionFactory connectionFactory) {

        RedisCacheConfiguration config = instanceConfig(60L);

        return RedisCacheManager.builder(connectionFactory)

                .cacheDefaults(config)

                .transactionAware()

                .build();

    }



    /**

     * 默认是1小时

     *

     * @param connectionFactory

     * @return

     */

    @Bean

    @Primary //不指定CacheManager的话默认就用这个CacheManager

    public RedisCacheManager cacheManager1Hour(RedisConnectionFactory connectionFactory) {

        RedisCacheConfiguration config = instanceConfig(3600L);

        return RedisCacheManager.builder(connectionFactory)

                .cacheDefaults(config)

                .transactionAware()

                .build();

    }



    /**

     * 1天过期

     *

     * @param connectionFactory

     * @return

     */

    @Bean

    public RedisCacheManager cacheManager1Day(RedisConnectionFactory connectionFactory) {



        RedisCacheConfiguration config = instanceConfig(3600 * 24L);

        return RedisCacheManager.builder(connectionFactory)

                .cacheDefaults(config)

                .transactionAware()

                .build();

    }



           /*目的是为了让缓存到Redis中的键值在可视化工具中不出现乱码(改了一下value的序列化方式为JSON字符串类型)

      * 并且可以是配置文件中除缓存时间的的配置项都可以生效

      * */

        private RedisCacheConfiguration instanceConfig(Long ttl) {

            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

            ObjectMapper objectMapper = new ObjectMapper();

            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

            objectMapper.registerModule(new JavaTimeModule());

            // 去掉各种@JsonSerialize注解的解析

            objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false);

            // 只针对非空的值进行序列化

            objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

            // 将类型序列化到属性json字符串中

        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,

                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);



        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);



        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig();

        //设置缓存过期时间

         cacheConfig=cacheConfig.entryTtl(Duration.ofSeconds(ttl));

        //设置缓存Value时的序列化机制为jackson2JsonRedisSerializer

        cacheConfig=cacheConfig.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));



        /*并且让配置文件中的配置也一起生效,否则配置文件中的内容就失效了,

        比如。我们设置的Key的前缀key-prefix: 啥的就无效了key-prefix,但是配置文件配置的过期时间时无效的,因为我们设置为自定义的了*/

         org.springframework.boot.autoconfigure.cache.CacheProperties.Redis redisProperties = CacheProperties.getRedis();

         if(redisProperties.getKeyPrefix()!=null)

         {

             cacheConfig=cacheConfig.prefixKeysWith(redisProperties.getKeyPrefix());

         }

         if(!redisProperties.isCacheNullValues())

         {

                cacheConfig=cacheConfig.disableCachingNullValues();

         }

         if(!redisProperties.isUseKeyPrefix())

         {

                cacheConfig=cacheConfig.disableKeyPrefix();

         }

         return  cacheConfig;

//        return RedisCacheConfiguration.defaultCacheConfig()

//                .entryTtl(Duration.ofSeconds(ttl))

//                //.disableCachingNullValues()

//                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));

    } 
   

在代码中指定cacheManager

@ResponseBody
@RequestMapping
("/testSpringCache")
@Cacheable(value = {"Cache"},key = "methodName+'_'+#size+'_'+#page",cacheManager = "cacheManager1Minute")

//这里指定的是cacheManager1Minute ,即缓存的过期时间是一分钟
public Object SpringCache(int size,int page)
{
   
return  size+page;
}

@ResponseBody
@RequestMapping
("/testSpringCache2")
@Cacheable(value = {"Cache"},key = "methodName+'_'+#size+'_'+#page",cacheManager = "cacheManager1Day")

//这里指定的是cacheManager1Minute ,即缓存的过期时间是一天
public Object SpringCache2(int size,int page)
{
   
return  size+page;
}

11.5 ⾃定义缓存KeyGenerator

直接使用@Cacheable 中的Key去设置key规则比较麻烦,SpringCache自定义Key规则使用KeyGenerator 
 
   
 /*我们在定义SpringCache的缓存Key的规则时,尽量不要让Key跟方法有太大关系,不然不好更新缓存,也不要让Key跟类名有关系,因为类名太长,可能会造成Bigkey影响Redis性能

* 比如 我们在一个方法中使用@Cacheable注解,并指定KeyGenerator,然后在另外一个方法中去用@CachePut注解更新,并指定相同的KeyGenerator,并且里面配置了Key跟方法名有关,那么@CachePut在另一个方法再更新缓存时,就会因为方法名不同就找不到要更新的key了,这样就无法更新到要更新的缓存

注意

1 我们可以设置多个KeyGenerator(一般也是这样做的),在具体指定哪个KeyGenerator时指定配置这个KeyGenerator的方法名即可

2 @Cacheable注解中keyKeyGenerator参数不可以同时存在会有冲突

以下为配置自定义的KeyGenerator在配置类中

//    自定义缓存Key的规则1(可以有多个

    @Bean

    public KeyGenerator SpringCacheCustomKeyGenenrtor1()

    {

        return new KeyGenerator() {

            @Override

            public Object generate(Object o, Method method, Object... objects) {

//  String key = o.getClass().getSimpleName() + "_" + method.getName() + "_" + StringUtils.arrayToDelimitedString(objects, "_");

// StringUtils.arrayToDelimitedString(objects, "_")这里objects表示方法的参数有多个所以是一个省略号参数,

// StringUtilsarrayToDelimitedString(objects,"_"),表示多个参数胡之间用"_"隔开

//  System.out.println(key);

//   return key;

                /*如果我们在@Cacheable注解中还指定了value参数则,最终存储的Key还是会加上value::*/

         String key= o.getClass().getName()+"_"+StringUtils.arrayToDelimitedString(objects,"_");

                System.out.println(key);

                return key;

            }

        };

    }

使用自定义的keyGenerator

@Cacheable(value = {"Cache"},keyGenerator="SpringCacheCustomKeyGenenrtor1",cacheManager = "cacheManager1Day")
public Object SpringCache3(int size,int page){}

11.6 SpringCache框架常⽤注解CachePut

CachePut (一般用于缓存的更新)(一般都是标注在Service层实现类上)

/*我们在定义SpringCache的缓存Key的规则时,尽量不要让Key跟方法有太大关系,不然不好更新缓存,也不要让Key跟类名有关系,因为类名太长,可能会造成Bigkey影响Redis性能

* 比如 我们在一个方法中使用@Cacheable注解,并指定KeyGenerator,然后在另外一个方法中去用@CachePut注解更新,并指定相同的KeyGenerator,并且里面配置了Key跟方法名有关,那么@CachePut在另一个方法再更新缓存时,就会因为方法名不同就找不到要更新的key了,这样就无法更新到要更新的缓存

根据法的请求参数对其结果进缓存(这个缓存结果一般是用来更新某个Key的值的)注意该注解每次都会触发真实法的调(与@Cacheable不同,@Cacheable:一旦缓存中有对应数据,就会调用方法

@CachePut注解的参数

value 缓存前缀名称,可以有多个(SpringCache生成的Key前会加上value::,故最终的Keyvalue::key

key 缓存的key规则,可以springEL表达式,默认是

法参数组合(也可以用KeyGenerator

condition 缓存条件,使springEL编写,返回true才缓存

使用实战

@ResponseBody
@RequestMapping
("/testSpringCache4")
@CachePut(value= = {"Cache"},keyGenerator="SpringCacheCustomKeyGenenrtor1",cacheManager = "cacheManager1Day")
public Object SpringCache4(int size,int page)

11.7 SpringCache框架常用注解CacheEvict

CacheEvict (用来删除缓存数据,(一般都是标注在Service层实现类上)

从缓存中移除相应数据, 触发缓存删除的操作,注意该注解每次都会触发真实法的调

CacheEvict 注解参数

value 缓存名称,可以有多个

key 缓存的key规则,可以springEL表达式,默认是法参数组合

beforeInvocation = false (一般使用这种)

缓存的清除是否在法之前执 ,

默认代表缓存清除操作是在法执之后执如果方法出现异常缓存就不会清除

beforeInvocation = true

代表清除缓存操作是在法运之前执法是否出现异常,缓存都会清除

使用实战

@ResponseBody
@RequestMapping
("/testSpringCache5")
@CacheEvict(value={"Cache"},keyGenerator="SpringCacheCustomKeyGenenrtor1",cacheManager= = "cacheManager1Day",beforeInvocation=false)
public Object SpringCache5(int size,int page)

11.8 SpringCache框架@Caching

@Caching用来组合多个Cache注解使 (一般都是标注在Service层实现类上)

(当一个方法要同时操作多个缓存数据时我们使用该注解,比如该方法要缓存一个Key1数据,删除key2数据,更新key3数据)

注意该注解每次都会触发真实法的调

允许在同⼀⽅法上使多个嵌套的@Cacheable@CachePut@CacheEvict注释实战

@ResponseBody
@RequestMapping
("/testSpringCache6")


@Caching(cacheable={@Cacheable(value={"Cache"},keyGenerator="SpringCacheCustomKeyGenenrtor1",cacheManager = "cacheManager1Day")},
 put={
@CachePut(value = {"Cache"},keyGenerator = "SpringCacheCustomKeyGenenrtor1",cacheManager = "cacheManager1Day")},
evict={
@CacheEvict(value={"Cache"},keyGenerator="SpringCacheCustomKeyGenenrtor1",cacheManager = "cacheManager1Day",beforeInvocation=false)}
)

public Object SpringCache6(int size,int page)

11.9 SpringCache解决缓存击穿,雪崩,穿透问题的方式

缓存击穿 (某个热点key缓存失效了)

缓存中没有但数据库中有的数据,假如是热点数据,那key在缓存过期的⼀刻,同时有⼤量的请求,这些请求都会击穿到DB,造成瞬时DB请求量⼤、压⼒增⼤

和缓存雪崩的区别在于这⾥针对某⼀key缓存,后者则是很多key

预防缓存击穿的方法

1 设置热点数据不过期

2 定时任务定时更新缓存

3 设置分布式互斥锁(利用Redisson

具体使用分布式互斥锁的步骤:

在查询数据库的方法中 互斥锁(不可使用共享锁),大量并发只让一个人(线程)去数据库中查询,其他人(线程)等待,第一个人查到以后把数据放一份到redis中再返回数据并释放锁,其他人获取到锁,先去缓存中查询,就会有数据,这样就不用去db中查。这样百万并发查询相当于只查了一次数据库--注意任何人得到锁以后,要先去redis中判断以下是否已经有缓存数据了,有就直接返回数据,没有再去数据库中查询或者设置热点数据永远不过期

SpringCache解决缓存击穿的的方式

@Cacheable使用参数sync   同步 sync,但是是本地锁)

sync 可以指示底层将缓存锁住,使只有⼀个线程可以,进⼊计算(当没有命中缓存时最多只有一个线程去执行方法),⽽其他线程堵塞,直到返回结果更新到缓存中。
 
  
这不能完全解决缓存击穿问题,因为仅仅是本地锁,只能控制当前节点中最多有一个线程(用户)去查数据库,无法像分布式锁那样控制所有节点一共只有一个线程(用户)去查数据库,但是其实在并发量不是巨大的情况下(因为集群中的每台服务器也就最多发送一次查询数据库的请求 ),也是可以接受的 (这种方式最方便,操作成本最低,开发效率高

具体使用

@Cacheable(cacheNames = {"Category"},key = "#root.method.name",sync = true)

缓存雪崩 (多个热点key都过期)

⼤量的key设置了相同的过期时间,导致在缓存在同⼀时刻全部失效,造成瞬时DB请求量⼤、压⼒骤增,引起雪崩

缓存雪崩预防

存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣

设置热点数据永远不过期,定时任务定时更新

SpringCache解决⽅案

设置差别的过时时间,⽐如CacheManager配置多个过期时间维度配置⽂件 time-to-live 配置

缓存穿透(查询不存在数据)

查询⼀个不存在的数据,由于缓存是不命中的,并且出于容错考虑,如发起为id“-1”不存在的数据

如果从存储层查不到数据则不写⼊缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。存在⼤量查询不存在的数据,可能DB就挂掉了,这也是⿊客利⽤不存在的key频繁攻击应⽤的⼀种⽅式。

预防

1 接⼝层增加校验,数据合理性校验

2 缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,设置短点的过期时间,防⽌同个key被⼀直攻击

3 使用分布式的布隆过滤器

SpringCache解决⽅案

空结果也缓存(默认是缓存空结果的),默认不配置condition或者unless

cache:

#使⽤的缓存类型type: redis #过时时间

redis:

# 是否缓存空结果,防⽌缓存穿透,默以为true

cache-null-values: true

11.10 Spring Cache的不足:

虽然可以解决缓存穿透 (默认可以缓存空数据)和缓存雪崩问题(加了过期时间即可,因为实际上每一个数据的存储的时间节点是不一样的,就算是设置的过期时间相同,也很难会一起过期(即最终的过期时间点是分散的))
但是无法完全解决缓存击穿问题,并且内部没有使用读写锁去解决缓存一致性问题,只是使用了一个过期时间去解决缓存一致性问题(无法实时的达到缓存一致性)
 
  
   解决:1 不使用Spring Cache,完全由我们自己去写缓存逻辑和锁逻辑
         2 使用 Spring Cache,并开启synchronize(同步锁)(默认是无锁状态的),但是这个锁是个本地锁,无法完全解决缓存击穿问题,不过其实在并发量不是巨大的情况下(因为集群中的每台服务器也就最多发送一次查询数据库的请求 ),也是可以接受的 (这种方式最方便
 
  
@Cacheable(cacheNames = {"Category"},key = "#root.method.name",sync = true)
 
  

3 使用 Spring Cache与分布式锁与少量的自己写的缓存逻辑相结合使用:可以完全解决缓存击穿问题 (这种方式完全解决了问题
具体的实现逻辑
我们把获取数据的方法抽取成3
1 总的获取数据的方法 里面调用使用分布式锁获取数据的方法(2 方法) 并加上@Cacheable注解
2 使用分布式锁获取数据的方法,里面写锁的逻辑和调用从数据库获取数据的方法
3 从数据库获取数据的方法(里面要加先判断是否缓存中有数据,有数据就直接返回。这是为了配合分布式锁,再后面因为锁的时序性,我们还有自己吧查到的数

11.11 Ehcache+redis组成多级缓存架构的实现以及问题

(实例写在 xdclass-shop的优惠卷服务上)
重难点是如何设置Key,我们理所应当的在SpringCache的配置文件中配置了KeyGenerator(可以有多个)
//在KeyGenerator中本地缓存key的规则我们最好设置为业务名: +用户ID couponrecord:userid:10
 
  
 
  

实例代码(写在 xdclass-shop的优惠卷服务)

/**
 *
自定义缓存key规则(缓存个人领卷记录)
 * @return
 
*/

@Bean
public KeyGenerator couponrecordKeyGenerator() {
   
return new KeyGenerator() {
       
@Override
       
public Object generate(Object o, Method method, Object... objects) {
            LoginUser loginUser = LoginInterceptor.
threadLocal.get();

 

//本地缓存key的规则我们最好设置为业务名:+用户ID
            String key = "couponrecord:" + "userid:" + loginUser.getId();
            System.out.println(key);
           
return key;
        }
    };
}

1 本地缓存数据+redis缓存数据

@GetMapping("PageSelectCouponRecord")
@ApiOperation("分页查询登录用户所有Coupon 数据接口2 ")
@Cacheable(value = "myCache",keyGenerator = "couponrecordKeyGenerator")//把数据缓存到本地并指定Key
public JsonData PageSelectCouponRecord(@ApiParam(value = "当前页码", required = true) @RequestParam(value = "page",defaultValue = "1")int page, @ApiParam(value = "每页显示多少条", required = true)@RequestParam(value = "size",defaultValue = "20") int size)
{
    String data =
stringRedisTemplate.opsForValue().get("couponrecord:" + LoginInterceptor.threadLocal.get().getId());
    System.
out.println(data);
   
if(StringUtils.isNotBlank(data))
    {
        Map map = JSON.parseObject(data, Map.
class);
        System.
out.println("从redis中获取个人缓存数据");
        System.
out.println("本地缓存个人领劵记录");
       
return  JsonData.buildSuccess(map);
    }
   
else{
        Map map =
this.couponRecordService.pageCouponRecordActivity(page, size);
       
long datatotal = (long) map.get("total_record");
       
if(datatotal>0) {
            String couponrecorddata = JSON.toJSONString(map);
           
stringRedisTemplate.opsForValue().set("couponrecord:" + LoginInterceptor.threadLocal.get().getId(), couponrecorddata);
            System.
out.println("个人领劵记录缓存到redis中");
       }
        System.
out.println("本地缓存个人领劵记录");

       
return  JsonData.buildSuccess(map);
    }
}

 
  
 
  

2 更新操作,同时更新本地缓存加redis缓存

@ApiOperation("领取优惠券")
@GetMapping("/add/promotion/{coupon_id}")
@CacheEvict(value = "myCache",keyGenerator = "couponrecordKeyGenerator") //删除指定key的本地缓存数据
public JsonData addPromotionCoupon(@ApiParam(value = "优惠券id",required = true) @PathVariable("coupon_id")long couponId){
    JsonData jsonData =
couponService.addCoupon(couponId, CouponCategoryEnum.PROMOTION);
   
stringRedisTemplate.delete("couponrecord:" + LoginInterceptor.threadLocal.get().getId());
   
return jsonData;
}

多级缓存架构的问题与注意事项(必看)(写在 xdclass-shop的网关服务上)

此外由于我们都会配置Nginx 进行负载均衡,一旦服务出现多节点的时候,极易出现本地缓存命中率低,和本地缓存数据不一致的情况,故我们必须把Nginx的负载均衡策略调整为固定分发,这样每个用户的请求就会固定分发到一台服务器上,就避免了本地缓存命中率低,和本地缓存数据不一致的情况。但是由于我们微服务项目的唯一请求入口是网关,而Nginx只能把请求根据IP固定分发到指定的GateWay网关节点,而本地缓存却都在微服务节点中,而请求经过网关还要做一次负载均衡把请求转发到微服务节点,一但微服务节点很多这样还是会造成本地缓存命中率低故仅对Nginx做固定分发是不够的,我们还要对GateWay网关节点做固定分发(这样做后实际我们就没必要让Nginx做固定分发了)。

GateWay网关节点做固定分发,要有三步,

1自定义GateWay网关负载均衡器,获取用户的请求ip,

  1. 自定义固定分发负载均衡策略(根据用户IP去做HASH

3把自定义GateWay网关负载均衡器和自定义固定分发负载均衡策略交给Spring框架去管理

具体代码如下:

package com.example.xdclassgateway.Config;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;

//1    自定义GateWay网关负载均衡器
public class UserIpLoadBalancerClientFilter  extends LoadBalancerClientFilter {

   
public UserIpLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
       
super(loadBalancer, properties);
    }

   
@Override
   
protected ServiceInstance choose(ServerWebExchange exchange) {
       
//这里可以拿到web请求的上下文,可以从header中取出来自己定义的数据。
       
String userId = exchange.getRequest().getHeaders().getFirst("userId");

        ServerHttpRequest request = exchange.getRequest();
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        InetAddress address = remoteAddress.getAddress();

       
//用户的Ip
       
String hostAddress = address.getHostAddress();
       
if (userId == null) {
           
return super.choose(exchange);
        }
       
if (this.loadBalancer instanceof RibbonLoadBalancerClient) {
            RibbonLoadBalancerClient client = (RibbonLoadBalancerClient)
this.loadBalancer;
            String serviceId = ((URI) exchange.getAttribute(
GATEWAY_REQUEST_URL_ATTR)).getHost();
           
//这里使用用户的Ip做为选择服务实例的key,这样用户就会有唯一对应的实例了

//在使用负载均衡的情况下,又是如何选择哪个实例提供服务呢--》通过RibbonLoadBalancerClient的choose方法来确认


             return client.choose(serviceId, hostAddress);
        }
       
return super.choose(exchange);
    }
}

package com.example.xdclassgateway.Config;
import java.util.List;
import org.apache.commons.lang.math.RandomUtils;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.Server;

/**
 *
 * @ClassName: GameCenterBalanceRule
 * @Description:
根据userip对服务进行负载均衡。同一个用户id的请求,都转发到同一个服务实例上面。
 * @author: wgs
 * @date: 2019年3月15日 下午2:17:06
 */

//2 自定义负载均衡策略根据用户IP去HASH从而选择对应的节点
public class GameCenterBalanceRule extends AbstractLoadBalancerRule {

   
@Override
   
public Server choose(Object key) {//这里的key就是过滤器中传过来的userip

       
List servers = this.getLoadBalancer().getReachableServers();

       
if (servers.isEmpty()) {
           
return null;
        }
       
//如果只有一个服务节点的话
       
if (servers.size() == 1) {
           
return servers.get(0);
        }
       
//如果没有得到userip
       
if (key == null) {
           
return randomChoose(servers);
        }
       
//对用户ip使用hash的方式去选择把请求转发到哪个实例上去
       
return hashKeyChoose(servers, key);
    }
   
/**
     *
     *

Description:随机返回一个服务实例


     * @param
servers
    
* @return
    

     *
     */
   
private Server randomChoose(List servers) {
       
int randomIndex = RandomUtils.nextInt(servers.size());
       
return servers.get(randomIndex);
    }
   
/**
     *
     *

Description:使用key的hash值,和服务实例数量求余,选择一个服务实例


     * @param
servers
    
* @param key
  
     *
     */
   
private Server hashKeyChoose(List servers, Object key) {
       
int hashCode = Math.abs(key.hashCode());
       
if (hashCode < servers.size()) {
           
return servers.get(hashCode);
        }
       
int index = hashCode % servers.size();
       
return servers.get(index);

    }

   
@Override
   
public void initWithNiwsConfig(IClientConfig config) {

    }
}

3把自定义GateWay网关负载均衡器和自定义固定分发负载均衡策略交给Spring框架去管理,把这个两个bean放入项目启动类中

@Bean
public UserIpLoadBalancerClientFilter userLoadBalanceClientFilter(LoadBalancerClient client, LoadBalancerProperties properties) {
   
return new UserIpLoadBalancerClientFilter(client, properties);
}


@Bean
public IRule iRule()
{
   
return  new GameCenterBalanceRule();
}

自定义GateWay网关负载均衡器具体参考文献            

https://www.cnblogs.com/wgslucky/p/10537561.html

Spring Cloud Gateway默认负载均衡策略的参考文献

http://www.ranxiao.top/2019/06/20/spring-cloud-gateway-default-load-balancer-rule

第十二章 redis持久化详解

12.1 Redis持久化介绍

Redis是⼀个内存数据库,如果没有配置持久化,redis重启后数据就全丢失

因此开启redis的持久化功能,将数据保存到磁盘上, 当redis重启后,可以从磁盘中恢复数据。

两种持久化⽅式

1 RDB (Redis DataBase)

2 AOF (append only file)

12.2 RDB持久化介绍

在指定的时间间隔内将内存中的数据集快照写⼊磁盘

默认的⽂件名为dump.rdb

产⽣快照的情况

1 save (不会用)

会阻塞当前Redis服务器,执⾏save命令期间,

Redis不能处理其他命令,直到RDB过程完成为⽌

2 bgsave

Redis主线程fork((fork()是linux函数)创建⼦进程(这个子进程专门用于RDB快照的生成),RDB持久化过程由⼦进程负责,会在后台异步进⾏快照操作,快照同时还可以响应客户端请求

3 ⾃动化

配置⽂件来完成,在配置文件中配置触发 Redis的 RDB 持久化条件

⽐如 "save m n"。表示m秒内数据集存在n次修改时,⾃动触发bgsave

4 主从架构(从节点同步数据时)

从服务器同步数据的时候,会发送sync执⾏同步操作,master主服务器就会执⾏bgsave

5 还可以手动执行命令生成RDB快照,

进入redis客户端执行命令save或bgsave可以生成dump.rdb文件, 每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件(当重启Redis的时候,Redis会自动解析dump.rdb文件中的数据到内存中)。

save与bgsave对比:

命令

save

bgsave

IO类型

同步

异步

是否阻塞redis其它命令

否(在生成子进程执行调用fork函

数时会有短暂阻塞)

复杂度

O(n)

O(n)

优点

不会消耗额外内存

不阻塞客户端命令

缺点

阻塞客户端命令

需要fork子进程,消耗内存

RDB持久化方式的优点

1 RDB⽂件紧凑,全量备份,适合⽤于进⾏备份和灾难恢复

2 在恢复⼤数据集时的速度⽐ AOF 的恢复速度要快

3 ⽣成的是⼀个紧凑压缩的⼆进制⽂件

缺点 1每次快照是⼀次全量备份,fork⼦进程进⾏后台操作,⼦进程存在开销

2在快照持久化期间修改的数据不会被保存,可能丢失数据

核⼼配置

dir 持久化⽂件的路径,AOF或者RDB持久化⽂件存储路径

dbfilename ⽂件名

AOF持久化介绍(appendonly yes,默认不开启)

12.3 AOF持久化介绍

(appendonly yes,默认不开启)

快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失 最近写入、且仍未保存到快照中的那些数据。从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方 式: AOF 持久化,将修改数据的每一条指令记录进文件appendonly.aof中

append only file,追加⽂件的⽅式(追加每条写命令到文件中),⽂件容易被人读懂

以独⽴⽇志的⽅式记录每次写命令, 重启时再重新执⾏AOF⽂件中的命令达到恢复数据的目的

写⼊过程宕机,也不影响之前的数据,因为可以通过 redis- check-aof检查修复问题

AOF的基本过程:每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文 件的末尾。 这样的话, 当 Redis 重新启动时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目 的。

AOF核⼼原理

1 Redis每次写⼊命令会追加到aof_buf(缓冲区) AOF缓冲区根据对应的策略向硬盘做同步操作(同步到appendonly.aof文件中)

2 ⾼频AOF会带来影响,特别是每次刷盘

AOF提供了3种同步⽅式,在性能和安全性⽅⾯做出平衡

appendfsync always

每次有数据修改发⽣时都会写⼊AOF⽂件,消耗性能多

appendfsync everysec(默认这种)

每秒钟同步⼀次,该策略为AOF的缺省策略。(故障时只丢失一秒的数据)

appendfsync no

不主从同步,由操作系统⾃动调度刷磁盘,性能是最好的,但是最不安全

(虽然操作系统的默认策略是每秒刷磁盘,但是也不一定,故可能出现很多数据的丢失

配置实战

1 appendonly yes,默认不开启

2 AOF⽂件名 通过 appendfilename 配置设置,默认⽂件名是appendonly.aof

3 存储路径同 RDB持久化⽅式⼀致,使⽤dir配置

12.4 AOF重写介绍

AOF重写的好处

1 AOF⽂件越来越⼤,需要定期对AOF⽂件进⾏重写达到 压缩

2 旧的AOF⽂件含有⽆效命令会被忽略(防止出现大量的没有用的,或者重复的指令),保留最新的数据命令

3 多条写命令可以合并为⼀个

4 AOF重写降低了⽂件占⽤空间

更⼩的AOF ⽂件可以更快地被Redis加载,提高了Redis重启时的恢复速度

注意,AOF重写redis会fork出一个子进程去做,不会对redis正常命令处理有太多影响

AOF重新的缺点:其实是一个小的缺点,AOF文件本来可读性是不错的,但是经过AOF重写压缩之后,可读性就变差了出现乱码(因为Redis4.x后默认支持和开启了混合持久化,将RDB快照和AOF的数据写在一起了

AOF重写触发的情况

1⼿动触发

直接调⽤bgrewriteaof命令

2 设置配置文件⾃动触发(如下两个配置可以控制AOF自动重写频率)

auto-aof-rewrite-min-size和auto-aof-rewrite- percentage参数

auto-aof-rewrite-min-size:默认 为64MB

表示运⾏AOF重写时⽂件最⼩体积,默认 为64MB。

/aof文件至少要达到64M才会可能自动重写,文件太小恢复速度本 来就很快,重写的意义不大

auto-aof-rewrite-percentage 默认为100%

代表当前AOF⽂件空间和之前最后⼀次重写后AOF⽂件空间(aof_base_size)的⽐值为多少时进行重写。(aof文件自上一次重写后文件大小增长了100%则再次触发重写)

12.5 AOFRDB的选择问题

Redis提供了不同的持久性选项:

1 RDB持久化以指定的时间间隔执⾏数据集的时间点快照。

2 AOF持久化记录服务器接收的每个写⼊操作,将在服务器启动时再次读取,重建原始数据集。使⽤与Redis协议本身相同的格式以仅追加⽅式记录命令,当⽂件太⼤时,Redis能够重写

RDB AOF ,我应该用哪一个?

命令

RDB

AOF

启动优先级

体积

恢复速度

数据安全性

容易丢数据

根据策略决定

启动优先级: 优先选择AOF去恢复数据到内存

恢复速度: 重启Redis恢复数据到内存时,RDF持久化方式比较块,因为解析二进制文件比较块,而AOF则是在重启Redis时一条一条的执行AOF文件中的命令假设有大量命令则执行的非常慢。

RDB的优缺点

优点:

RDB最⼤限度地提⾼了Redis的性能,⽗进程不需要参与磁盘I/O ork⼦进程进⾏后台操作,⼦进程存在开销

RDB⽂件紧凑,全量备份,适合⽤于进⾏备份和灾难恢复

在恢复⼤数据集时的速度⽐ AOF 的恢复速度要快

⽣成的是⼀个紧凑压缩的⼆进制⽂件

缺点:

如果您需要在Redis停⽌⼯作时(例如断电后)将数据丢失的可能性降⾄最低,则RDB并不好

RDB经常需要fork才能使⽤⼦进程持久存储在磁盘上。如果数据集很⼤,Fork可能会⾮常耗时

AOF的优缺点

优点:1 数据更加安全

2 当Redis AOF⽂件太⼤时,Redis能够在后台⾃动重写AOF

3 AOF以易于理解和解析的格式,⼀个接⼀个地包含所有操作的⽇志

缺点:

1 AOF⽂件通常⽐同⼀数据集的等效RDB⽂件⼤

2根据确切的fsync策略,重启恢复数据的时候AOF可能⽐RDB 慢

在线上我们到底该怎么做?

1 RDB持久化与AOF持久化⼀起使⽤

2 如果Redis中的数据并不是特别敏感或者可以通过其它

3 ⽅式重写⽣成数据

4 集群中可以关闭AOF持久化,靠集群的备份⽅式保证数据丢失性降低(从节点会向主节点同步数据)(但是最好还是开启)

5 自己制定策略定期检查Redis的情况,然后可以⼿动触发备份、重写数据;

采⽤集群和主从同z

12.6混合持久化:

重启 Redis 时,我们很少使用 RDB来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重 放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很 长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。

Redis4.0后开始的AOF重写时⽀持混合模式持久化(默认开启)(所以才会出现AOF文件中的数据出现乱码)

1 就是rdb和aof⼀起⽤

2 直接将rdb持久化的⽅式来操作将⼆进制内容覆盖到aof⽂件中,rdb是⼆进制,所以很⼩

3 重写后如果还有写⼊的话还是继续append追加⽂件原始命令到AOF⽂件中,等下次⽂件过⼤的时候再次rewrite

4 默认是开启状态

好处:

1 混合持久化结合了RDB持久化 和 AOF 持久化的优点,采取了rdb的⽂件⼩易于灾难恢复,重启时恢复速度也快

2 同时结合AOF,增量的数据以AOF⽅式保存了,数据更少的丢失

坏处

AOF文件中有一部分是RDB格式,是⼆进制,所以阅读性较差

混合持久化的具体过程

如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将 重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一 起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改 名,原子的覆盖原有的AOF文件,完成新旧两个AOF文件的替换。 于是在 Redis 重启的时候,可以先加载 AOF文件中的RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅得到提升

混合持久化AOF文件结构

重启Redis后数据恢复的过程

先看是否存在aof⽂件,若存在则先按照aof⽂件恢复,aof⽐rdb全,且aof⽂件中也有rewrite成rdb⼆进制 格式的内容

若aof不存在,则才会查找rdb文件是否存在

第十三章 Redis的IO多路复用

Redis 单线程为什么还能这么快?

因为它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性 能损耗问题。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如 keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。

Redis 单线程如何处理那么多的并发客户端连接?(Redis单线程可以同时处理多个客户端的并发连接,这不是指可以同时执行多个客户端发来的命令,多个客户端发来的命令还是一个一个的执行)

Redis的IO多路复用:redis利用epoll(NIO模型)来实现IO多路复用,将连接信息和事件放到队列中,依次放到 文件事件分派器,事件分派器将事件分发给事件处理器。

多路指的是多个socket连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

如图所示

redis出现堆外内存溢出OutOfDirectMemoryError,这个内存溢出不是指的JVM内存

springboot2.0以后默认使用lettuce作为操作redis的客户端(用来连接Redis的工具),它使用Netty进行网络通信,lettuce的bug导致没有及时清理掉无用的内存,达到了netty设置的内存限制,会造成高并发下堆外内存溢出问题

如果Netty没有指定堆外内存,会默认使用JVM配置的堆内存(-Xmx 即服务器能占用的最大内存),JVM内存越大,出现堆外内存溢出的时间越晚,但迟早会出现,lettuce底层会自己计数,一旦内存超过它的最大限制,就会抛出异常

所以只调大JVM内存是不能根源解决这个问题

解决方案:

(lettuce,Jedis都是用来连接和操作Redis的工具 相当于jdbc)

1、升级lettuce客户端 ,由于使用netty进行网络通信的架构,具有很好的性能和高吞吐量 5.2版本已经不会有这个bug了

2、切换使用Jedis(老版的jedis客户端,很久没有更新)相对较慢

无论是lettuce还是jedis操作redis客户端,他们底层都被spring再次封装成RedisTemplate,和SpringRedisTemplates,我们直接使用RedisTemplates 或者SpringRedisTemplates操作客户端即可

你可能感兴趣的:(redis学习,java,redis,redisson,nosql)