Redis简介
- redis是单线程操作(6.0后增加了多线程支持)
- 解压缩得到的
zip
格式的安装包 - 得到一个
bin
文件夹redis-server
是启动redis服务的程序,可以添加配置文件路径,以使用指定的配置进行启动- 启动会得到版本信息,端口号
**6379**
,配置文件redis.conf
的路径
- 启动会得到版本信息,端口号
redis-cli
是redis的客户端,使用前要先打开redis服务- 得到
redis
的地址(本地)
- 得到
redis.windows.conf
是配置文件,linux下为redis.conf
shutdown
中断redis服务
Redis的配置
- 配置信息除了在
**redis.conf**
中查看修改,也可以通过redis-cli
里修改 - 配置详细作用见链接
- 获取所有配置信息:
config get *
- 获取指定配置项信息:
config get 名称
设置配置值
config set 名称 新值
1) "loglevel" //这就是一个键和值
2) "notice"
配置详解
io-threads-do-reads yes/no 是否启用多线程操作
io-threads 4 #多线程数,官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程;开启多线程后,还需要设置线程数,否则是不生效的。
持久化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项
aof-use-rdb-preamble
开启redis是内存数据库,服务进程退出数据消失
- rdb是默认持久化方式,生成的是dump.rdb文件
- 所以适合大规模数据恢复,因为不影响父进程
- 缺点就是完整性不是非常高,可能还没触发就忽略宕机。同时子进程会占用一些空间
aof适合作为主备份,rdb适合从备份,如从机上使用rdb,或者作为aof出错后的后备备份,
配置
在指定时间间隔内将内存中数据集快照写入磁盘,即Snapshot快照。恢复时将快照文件读取到内存中即可
触发机制
如下操作时会触发持久化
- save规则满足时触发
- 执行了flushall时
-
恢复
将rdb文件放于redis目录下,redis启动时会自动检查dump.rdb然后恢复
AOF
AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF,可以通过 appendonly 参数开启
appendonly yes
然后重启- 开启AOF 持久化后每一条写命令都会被记录(读命令不记录),Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的
.aof
文件 - aof如果被意外修改,那么再次启动redis是无法连接的,可以使用
redis-check-aof --fix
进行修复
- 开启AOF 持久化后每一条写命令都会被记录(读命令不记录),Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的
- aof还有一个缺点是数据文件远大于rdb,恢复,修复速度也慢于rdb
配置
- 为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。
- 而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。
- 当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。 ```nginx appendfsync always #每次有写操作都会写入AOF文件,这样会严重降低Redis的速度 appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘,可能会丢失一秒数据 appendfsync no #让操作系统决定何时进行同步
还有些aof的重写配置,如限制配置文件大写,如果达到一定大小就新建一个线程重写,避免一个进程的文件无限制追加数据 auto - aof - rewrite-min-size 64mb
<a name="JQOMd"></a>
# 缓存原理
<a name="tRfMn"></a>
## 可能出现的问题与解决
下面场景应该是缓存数据来源于数据库的情况,即缓存与数据库进行了关联,保持一致性的前提
- 缓存穿透:**缓存和数据库都没有。**大量请求跳过缓存直接到数据库查询
- 缓存击穿:**单个数据缓存不存在(一般是失效)**,数据库还有相关数据。大量请求到数据库查该数据
- 缓存雪崩:**大量数据缓存不存在(一般是失效,宕机)**,大量请求查数据库
---
- 缓存穿透可以使用布隆过滤器,将肯定不存在的请求提前拦截
- 可以借助springCache将数据库和缓存都不存在的数据直接缓存个null值,使用该配置`spring.cache.redis.cache-null-values=true` 注意这个缓存要设置过期时间,一般十几秒。太长导致正常的缓存也无法使用
- 缓存击穿和雪崩在无外力影响下一般都是过期导致的
- 击穿的一般是常用的少量数据,即热点数据,**可以设置热点数据永不过期,或者使用互斥锁,使得一个key同一时间只有一个线程能访问**
- 可以借助springCache直接绑定一个方法处理该热点数据
- @Cacheable注解提供了一个新的参数“sync”(boolean类型,缺省为false),当设置它为true时,只有一个线程的请求会去到数据库,其他线程都会等待直到缓存可用
- 雪崩主要由应用程序导致,设置了大量过期时间相同的数据,**可以采用限流和集群redis的方式解决,或者调整业务逻辑,再或者如果是预料到要面对大量请求,我先提前演练一遍把数据生成出来**
<a name="hFyd5"></a>
## 缓存读取机制
- 缓存机制:数据先查缓存,存在就返回。
- 不存在就查数据库,数据库存在更新缓存,然后返回;不存在则返回空
- ![](https://cdn.nlark.com/yuque/0/2022/png/2319994/1645079413432-54a1776f-d87c-4aa2-b4f1-b58b181f2887.png#clientId=ub2d0a506-a475-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=242&id=u5ffbf30e&margin=%5Bobject%20Object%5D&originHeight=728&originWidth=626&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=shadow&taskId=u06b679c3-b3f0-4d08-a9c1-3d21eb6427b&title=&width=208)
<a name="DPwAb"></a>
## 缓存读写策略
- 常用的就三种,这3 种缓存读写策略各有优劣,不存在最佳,需要我们根据具体的业务场景选择更适合的。
<a name="YqCFM"></a>
### 旁路缓存模式
- **我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。**
- **服务端以DB数据为准,**同时维系 DB 和 cache
- 读:从缓存中读,读到就返回。没读到就就从 DB 中读取数据返回,再更新cache
- 写:更新数据库中的记录;删除缓存记录。(下次读的时候再生成缓存数据)
- 旁路缓存可以解决双写并发问题和绝大部分读写并发问题(读写并发发生的几率很低,即读操作时缓存刚好失效,于是读操作进入数据库后又发生了写缓存操作)
- 劣势:
- **首次请求数据一定不在 cache 中,必须走db**
- **写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 (即大量查询没有走cache)**
---
- 劣势解决:
- 数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。
- 可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小
- 读写并发可以将读时刷新缓存与写时删除缓存的任务交给消息队列去做,队列删除刷新期间无其他人干扰,单线处理
<a name="Bvhyk"></a>
### 读写穿透
- 服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责
- 平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入DB的功能。
- 读:从 cache 中读取数据,读取到就直接返回。读不到查db然后写入缓存再返回
- 写:先查 cache,cache 中不存在,直接更新 DB。cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB
<a name="YtWwX"></a>
### 异步缓存写入
- 和读写穿透很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。
- 但是,两个又有很大的不同:**Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。**
- 这种方式对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新DB的话,cache服务可能就就挂掉了。
- 异步缓存写入的DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景
<a name="AxmQk"></a>
## 布隆过滤器
- 擅长从超大的数据集中快速告诉你查找的数据存不存在(以防止缓存穿透)
- **返回存在不一定就真的存在,不过返回不存在,则一定不存在。**
- **这是由于不同的数据生成的哈希可能相同导致的**
- 用于缓存与本地数据库前,**即请求先查布隆,然后才是缓存和本地数据库**
---
- 原理**:直接通过索引访问而不是常规的遍历判断,又是使用位数组保存数据,使得空间和速度都远胜一般的算法。缺点是可能误判,删除困难(因为一个索引可能跟多个数据存在关联)**
- 一个很长的bit数组,初始化时都是0。每当要存一个数时,**就用一系列hash函数对该数生成几个int哈希值。然后在bit数组的这几个hash值的索引位置上将值改为1**。
- 如果要检索一个值是否存在,则根据生成的hash值索引去bit数组上找这几个索引的值,如果都为1,则极有可能存在,如果这几个索引的值存在0,则不存在。
- **可以通过增加数组长度,或者是增加哈希函数的数量,或者选择更可靠的哈希函数减少误判率**
---
- 应用场景:
- 检查单词拼写正确性
- 黑名单检测
- 垃圾邮件过滤
- 搜索爬虫URL去重
- 缓存穿透过滤
```java
package com.example.demo.xx;
import java.util.BitSet;
/**
* 位数组的大小
*/
public class MyBloomFilter {
private static final int DEFAULT_SIZE = 2 << 24;
/**
* 通过这个数组可以创建 6 个不同的哈希函数,即这六个作为不同的盐生成6个哈希方法
*/
private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};
/**
* 位数组。数组中的元素只能是 0 或者 1
*/
private BitSet bits = new BitSet(DEFAULT_SIZE);
/**
* 存放包含 hash 函数的类的数组
*/
private SimpleHash[] func = new SimpleHash[SEEDS.length];
/**
* 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
*/
public MyBloomFilter() {
// 初始化多个不同的 Hash 函数
for (int i = 0; i < SEEDS.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
}
}
/**
* 添加元素到位数组
*/
public void add(Object value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
/**
* 判断指定元素是否存在于位数组
*/
public boolean contains(Object value) {
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
/**
* 静态内部类。用于 hash 操作!
*/
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
/**
* 计算 hash 值
*/
public int hash(Object value) {
int h;
return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
}
}
}
//----------------------测试-------------------------------
String value1 = "https://javaguide.cn/";
String value2 = "https://github.com/Snailclimb";
MyBloomFilter filter = new MyBloomFilter();
System.out.println(filter.contains(value1));
System.out.println(filter.contains(value2));
filter.add(value1);
filter.add(value2);
System.out.println(filter.contains(value1));
System.out.println(filter.contains(value2));
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
// 创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
1500,
0.01);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
布隆过滤器的不足及优化
- 布隆过滤器的删除十分困难。而所有的key又都会保存到布隆里,我们可能活跃的key只是所有key的很小一部分。key越来越多导致误判的几率越来越高
缓存淘汰机制
- 有的地方叫做内存淘汰机制
- 为了节约内存,防止数据只进内存却不出导致内存不足会对过期的数据进行淘汰:具体如下
- 随机删除:缓存过期只是设置了个过期的标记,redis采用定时删除的方式,每次随机删除一部分过期的。(原因是当redis里数据量很大时,全部扫描一遍找出所有过期的是很耗时的)
- 惰性删除:缓存被查询时如果是过期的,则立即删除。(防止有的缓存过期却运气好一直没被随机扫描到)
- 即便有上面2种策略,依旧有可能存在过期数据既没被扫描,又没被查询到。而且还可能新增速度大于删除速度,还有如下方案:
- Redis 提供 6 种数据淘汰策略:
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
- Redis 提供 6 种数据淘汰策略:
4.0 版本后增加以下两种:
1. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
1. **allkeys-lfu(least frequently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
发布订阅
- 即redis实现类似消息队列的功能。有消息发送者(生产者)
**PUBLISH**
,消息订阅者(消费者)**SUB**
,频道名即队列名称 ```nginx subscribe 频道名 #订阅一个频道(自动创建队列) 1) “subscribe” 2) “news” 3) (integer) 1
publish 频道名 消息 #向一个频道发消息,所有订阅了这个频道的redis-cli都会自动接收并展示消息 其他订阅了该频道的自动展示如下信息 1) “message” #表示这是一个消息 2) “news” #频道名 3) “heell” #接收的消息 unsubscribe 频道 #退订
<a name="ABa9O"></a>
# 主从复制
- **主从复制是集群的基础**,主机与从机配合**,一般是为了解决读写分离,负载均衡,故障恢复等问题。**
- **集群最低为一主二从**,因为一个从机时哨兵机制无法进行选举
- **主机可以有0个以上的从机,但是从机只能有一个主机**
- **主从环境配置好后,主机只能写,从机只能读,从机 无法进行写操作,主机的写信息会自动同步到从机中(数据只能由主机到从机。)**
- **如果主机服务中断,从机依旧是从机,仍旧能读取,但是不会有新数据被写入,需要配置哨兵才能实现主从切换。**
- **注意如果是命令关联的从机,从机断开再上线是会变成主机的! 但是重新关联后主机数据全部同步过去**
- **即刚关联上时,主机数据会全部进行同步,已经关联后数据变化只同步变化的数据**
<a name="tLfna"></a>
## 环境配置
- 即需要有多个配置文件,启动服务时使用不同的配置文件。而我们配置时**一般只需要配置从库即可,因为redis连接默认就是主库模式**
- 不同的conf一般改为不同的端口,不同pid,不同日志文件名,不同rdb文件名即可。
<a name="uCZPz"></a>
### 层层链路
- 这是主从复制除开一主多从的模型外另外的一种模型。**(一个从节点只能有一个主节点决定了不可能有多主多从模型)**
- 层层链路即`A(主) ——> B——> C`,b`slaveof`A ,C`slaveof`b。这时的B使用`info replication`查看依旧还是从节点,(即不能往b中写数据,但是b可以往c中写数据)
- 最终的效果是**a进行写入,b,c都会出现写入的信息**
<a name="Pcfri"></a>
## 关联主从机
- `info replication`(复制的意思)查看当前库的主从复制的信息,**从机执行**`**slaveof IP地址 端口**`**即可成为一个主机的从机**
- **命令行执行**`slaveof`只是暂时生效,配置文件配置主从信息才能永久生效
```shell
role:master #当前的角色,默认为主库,slave则为从机
connected_slaves:0 #从机数量
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
master_host: #只有从机才有该选项,显示主机的地址
master_host: #只有从机才有该选项,显示主机的端口
slave... host...port...state... #只有主机才有该选项,显示从机的地址,端口,状态等(state为online表示在线)
在从机的配置文件找到如下2个位置
replicaof <masterip> <masterport> 这里配置从机的父节点
masterauth <master-password> 如果父节点有密码则配置密码
主从切换
主机出现故障,可以使用
slaveof no one
可以将一个从机切换为主机,然后将修改其他从机链接到新的主机。如果原来的主服务器恢复,可能又要将新的主服务器和从机链接回原机,很麻烦哨兵模式
当主服务器宕机后,自动实现将一台从服务器切换为主服务器
- 哨兵即一个独立的进程,通过发送命令,让服务器返回状态,从而监控每一个服务器的状态。如果出现主服务器宕机,就通过
发布订阅模式
通知其他服务器修改配置文件,切换主机 - 一个哨兵容易挂,我们可以弄多个哨兵,下图2有6个进程,3个哨兵-服务器进程,3个哨兵-哨兵进程,此时哨兵机制为:(failover即切换主机),票数高的从服务器成为新主机
-
优缺点
接口的幂等性
接口的幂等性问题指同一个请求多次重复提交,结果不一致
- 所以接口的幂等性就是指
接口可重复调用
删除和查询天然是幂等的- 如在无其他请求介入的情况下,对一个请求执行多次删除和查询,结果是不变的。而修改可能是基于获取的结果进行变化,如执行金额+100.多次执行不幂等。新增影响 了结果集
- 所以接口的幂等性就是指
- 修改操作实现幂等性:可以通过sql改造(不要更新不确定的值,直接写死变化为几)和
last_updated_at
字段结合实现幂等。或者通过token和去重表实现 新增操作实现幂等性:可以通过设置个标识,执行第一次变化该标识,后续操作如果标识发生变化不进行操作
Redis实现分布式锁
分布式锁简介
分布式锁即多个节点同时处理一个问题可能导致错误数据,这时需要一个公共的锁去控制
实现分布式锁可以使用Zookeeper,但是在无zookeeper时不可能仅仅为了实现分布式锁而专门引入 Zookeeper
使用set nx实现分布式锁大致逻辑如下:下面代码存在2个问题
- 超时自动失效是为了防止一个线程一直持有锁不释放。但是关键在于锁的时长应该设置多少,长了影响效率,短了业务没执行完就释放了锁
- 解锁时,查 - 删 操作是 2 个操作,由两个命令完成,非原子性
- 我们可以使用
Redisson
解决上述问题,Redisson
自动帮助我们解决上述问题String uuid1 = ...;
// lock
set Test uuid1 NX PX 3000
try {
// ....
} finally {
// unlock
String uuid2 = get Test;
if (uuid1.equals(uuid2) {
redisTool.del('Test');
}
}
使用Redisson
依赖
```xmlorg.redisson redisson 3.15.6
<a name="cP3fg"></a>
### bean配置
```java
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// config.setLockWatchdogTimeout();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setKeepAlive(true);
return Redisson.create(config);
}
使用
- Redisson的锁都是可重入锁,一个线程可以对一个锁反复上锁时,逻辑上,你应该对它执行同样次数的解锁。
- redisson给线程上锁后,其他线程在尝试获取该锁时,如果当前使用线程未释放该锁,则会进行等待该锁被释放(进入阻塞队列)
- 如果你不愿意其他线程阻塞,那么其他线程可以调用
tryLock()
方法上锁而不是lock
。tryLock
上锁会立刻(或最多等一段时间)返回,而不会一直等(直到所得持有线程释放)
- 如果你不愿意其他线程阻塞,那么其他线程可以调用
lock()可以指定过期时间,到期自动解锁(解锁操作即删除该键),如果未设置过期时间,那么只有执行unlock时该键才会被删除
lock(10, TimeUnit.SECONDS);
设置自动过期时间@Autowired
RedissonClient redissonClient;
@Test
void contextLoads() throws InterruptedException {
for (int i = 0; i < 5; i++) {
final long seconds = i;
Thread t = new Thread(() -> {
RLock hello = redissonClient.getLock("hello");
hello.lock(); //其他线程走到这一步,必须等待当前正在睡觉的线程起来释放锁后才能准备睡眠,所以结果是一个一个排队,上一个睡好后再睡下一个。
//如果去掉上锁,那么多个线程就会同时进行睡眠,不会进行等待
Long a=System.currentTimeMillis();
log.info("lock success。准备睡 {} 秒,再起来释放锁", seconds * 2);
try {
Thread.sleep(seconds * 2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
hello.unlock();
log.info("过去了"+(System.currentTimeMillis()-a)/1000);
log.info("成功释放锁");
});
t.start();
}
Thread.sleep(300000); //这里是防止其他线程依赖redisson,但是主线程已经结束把redisson关闭,就会其他线程会报redisson被关闭的错
}
```java // 拿不到就立刻返回 hello.tryLock();
// 拿不到最多等 1 秒。1 秒内始终拿不到,就返回 hello.tryLock(1, TimeUnit.SECONDS);
// 拿不到最多等 1 秒。1 秒内始终拿不到,就返回。 // 如果拿到了,自动在 10 秒后释放。 hello.tryLock(1, 10, TimeUnit.SECONDS); ```
原理
- 上锁:Redisson 在上锁时,向 Redis 中添加的键值对的键是
UUID
+thread-id
拼接而成的字符串;值是这个锁的上锁次数。- 执行上锁操作时,Redisson 会判断你是否是锁的持有者(即当前线程的ID与键值里的id是否一样) 一致则允许重入锁,不一致则表示是其他线程,那么进入等待
- 上锁线程业务未执行完自动延长锁的时长的原理:Redisson 中客户端一旦加锁成功,就会启动一个后台线程(惯例称之为
**watch dog **
看门狗)。watch dog 线程默认会每隔 10 秒检查一下,如果锁 key 还存在,那么它会不断的延长锁 key 的生存时间,直到你的代码中去删除锁 key 。- Redisson 看门狗(watch dog)在指定了加锁时间时(
tryLock(long)
),是不会对锁时间自动续租的。 lock/tryLock
未指定过期时间时,redisson会自动设置默认过期时间30秒
可以配置- 但是由于redisson还有自动续期的操作,所以30秒后锁不会被释放(除非watch dog线程挂了才会结束自动续期)
- Redisson 看门狗(watch dog)在指定了加锁时间时(
- Redisson解决
**查-删**
非原子性:redisson上锁与解锁通过lua脚本实现,redis执行lua脚本是原子性的,执行一个lua脚本期间不会再执行其他命令