redis基础学习(一)
2024-07-18 18:16:02 # redis # 学习笔记

redis基础学习(一)

基本介绍

功能

  1. 分布式缓存:redis的数据操作主要在内存,是kv数据库的一种

  2. 内存存储和持久化,redis异步将内存数据写到磁盘上

  3. 高可用架构:单机,主从,哨兵,集群

  4. 缓存击穿,雪崩,击穿

  5. 分布式锁

  6. 队列

  7. 排行+点赞

alt text

优势

  • 性能高
  • 数据类型丰富
  • redis支持数据持久化,内存中数据保存到磁盘,重启的时候再次加载使用
  • redis支持数据备份

alt text

redis的十大数据类型

  1. string

  2. list:双端链表

  3. hash

  4. set:集合,通过哈希表实现,集合中不能出现重复数据

  5. zset:有序集合,每个元素关联一个double类型的分数,通过分数进行排序

  6. GEO:地理信息:

     添加地理位置的坐标。
    
     获取地理位置的坐标。
    
     计算两个位置之间的距离。
    
     根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
    
  7. HyperLogLog:基数统计,在redis中,每个HyperLogLog只需要花费12kb的内存,元素越多,耗费的内存却是很小的,但是不会存储元素本身,只会根据输入的元素来计算基数

  8. bitmap:位图 0和1 表现的二进制的bit数组

  9. bitfield:位域,一次性对多个比特域进行操作

  10. Stream:流

常见操作

alt text

redis数据类型使用

这里现用现查

redis的持久化机制

防止内存中的数据丢失

备份和恢复

数据迁移

RDB

全量快照,实现把某一时刻的数据和状态写道磁盘中

优点

适合大规模的数据恢复,对业务定时备份,对数据完整性和一致性要求不高的场景,RDB文件在内存加载速度比AOF快很多

缺点

每隔一段时间机械能一次备份,如果redis死掉,就会丢失当前到最近一次快照期间的数据,快照数据会丢失

内存数据全量同步,使用资源太大

什么时候会触发RDB快照

  1. 配置文件中的配置
  2. 手动save保存
  3. 主从复制,主节点触发
  4. 执行shutdown适合没有设置开启AOF持久化

AOF

日志的形式来记录所有的写操作,只记录写操作,重启就按照写操作的命令重新执行一次

默认是不开启AOF的

AOF持久化工作流程

请求命令会先写到一个AOF缓存中保存,然后根据配置的AOF缓存策略再写入到AOF文件,服务器重启适合会从AOF文件载入数据

AOF缓存写入策略

  • Always:每条命令都写入AOF缓存中,然后同步到磁盘。性能影响大
  • Everysec:每秒写入。可能丢失一秒数据
  • No:操作系统控制写入。可能丢失很多数据

优点

性能高可以做紧急恢复

缺点

AOF文件比RDB文件大,恢复速度慢

AOF运行效率比RDB慢

RDB+AOF的混合持久化

使用RDB做全量快照,AOF做增量备份

先用RDB做全量快照存储,AOF持久化记录所有的写操作,当重写策略满足的适合,将最新的数据存储为新的RDB。重启服务的适合就会从RDB和AOF两部分恢复数据

纯缓存模式

建议不要

关闭RDB和AOF

redis事务

事务中所有的命令都会序列化,顺序的执行,不会被其他命令插入

redis的事务和传统数据库的事务不同,不一定是一起成功或者一起失败

redis管道

如何优化频繁命令往返带来的性能瓶颈

这个问题是由于redis发送命令是这样的:

首先发送命令,然后命令排队,命令再执行,执行后返回一个结果应答,然后下一条命令再执行,这样中间就来回的应答频繁io

这里就可以使用redis的管道

概况

pipeline是将命令打包一次性发送

pipeline支持批量执行不同的命令

管道和事务对比

事务有原子性,管道没有原子性

管道是一次性发送多条命令,事务是一条一条发送

事务会堵塞其他命令,管道不会

redis复制

主从复制:master以写为主,slave以读为主

master数据变化的时候,会将新数据异步同步到slave数据库

支持什么

  • 读写分离
  • 容灾恢复
  • 数据备份
  • 水平扩容

使用

只需要配置从数据库,不需要配置主数据库

复制原理

从数据库首次启动会连接主数据库与进行一次全量复制,slave原有的数据会覆盖

master收到要复制的命令会先RDB一个快照,同时手机所有的修改数据集的命令缓存,然后把RDB文件和这些命令都给从数据库

master10秒和slave进行一次心跳检测

master继续将修改命令发给slave

redis哨兵模式

检查后台的maser死没死,死掉就投票换一个从数据库

首先某个slave被选为新的master,规则是:

在配置文件中的,按照优先级进行选举,复制偏移量offset最大的从节点

然然后其他节点变成他的从节点,就算老节点恢复了,也变成从节点

redis集群

数据量过大,单个master没法支撑,所以要组成集群

集群模式支持多个master,每个master下面还有多个

集群模式自带故障转移,所以不需要再配置哨兵模式

集群算法

redis的集群分配是根据槽位来分配的

分槽和分片:
alt text

这种分槽和分片很适用于扩容和数据分片查找

Hash槽= CRC16(KEY) % 16384

redis单线程和多线程

redis其实整体来看是多线程的,但是redis的持久化RDB或者AOF,异步删除,集群数据同步,这些都是额外的线程来执行的,命令的工作是单线程的

老版本的redis是单线程的,新版本是多线程的

redis单线程的问题

大key:如果一个key特别大,那么删除key的时候就会堵塞

redis的多线程特性和IO多路复用

redis的主要瓶颈就是内存,网络IO

redis现在版本的多线程就是来处理网络请求的,多IO线程来处理网络请求,但是读写操作还是单线程来处理

IO多路复用:

I/O多路复用,就是通过一种机制,让一个线程可以监视多个链接描述符,一旦某个描述符就绪(一般都是读就绪或者写就绪),就能够通知程序进行相应的读写操作。这种机制的使用需要select 、 poll 、 epoll 来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

它的概念是:多个Socket链接复用一根网线,这个功能是在内核+驱动层实现的。
alt text

redis的Morekey问题

Morekey会让查询或者删除都做的很慢

优化:在配置里面使用惰性释放
也就是多线程来异步删除数据

redis的一致性

缓存双写一致性

如果redis有数据就需要和数据库的值相同

如果redis没有数据,数据库中如果是最新值就要回写到redis中

缓存操作

只读缓存

读写缓存:

  • 同步读写缓存
    • 写数据库的同时也要写redis缓存,缓存和数据库中的数据一致
    • 如果要缓存和数据库中的数据一致就要使用同步读写缓存
  • 异步读写缓存
    • 正常业务,如果mysql数据变动了,可以允许一定时间后作用到redis
    • 异常情况出现,需要借助kafka等中间件来重写

双缓加锁

这个是为了防止多个线程同时查询相同的数据项,数据项步骤缓存中,所有线程都会查询数据库更新缓存.避免缓存穿透

多个线程同时去查数据库的这条数据,我们可以在第一个查询数据的请求上加一个互斥锁,其他线程到这一步拿不到锁就等着,等第一个线程查询到了数据然后做缓存,后面的线程进来发现有缓存了就走缓存

详细说明

  1. 第一次检查缓存:

每个线程首先检查缓存中是否存在所需的数据。

  1. 获取互斥锁:

如果缓存中没有数据,第一个线程获取互斥锁,防止其他线程同时查询数据库。

  1. 第二次检查缓存:

拿到锁的线程在查询数据库之前再检查一次缓存,以防其他线程在此期间已经更新了缓存。

  1. 查询数据库并更新缓存:

如果第二次检查缓存仍然没有数据,则查询数据库并更新缓存。

  1. 释放锁:

更新缓存后,释放互斥锁,其他线程可以继续获取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CacheService {

private final Map<String, String> cache = new ConcurrentHashMap<>();
private final Lock lock = new ReentrantLock();

public String getValue(String key) {
// 第一次检查缓存
String value = cache.get(key);
if (value != null) {
return value;
}

// 获取互斥锁
try {
if (lock.tryLock()) {
try {
// 第二次检查缓存
value = cache.get(key);
if (value == null) {
// 查询数据库
value = queryDatabase(key);
// 更新缓存
cache.put(key, value);
}
} finally {
// 释放锁
lock.unlock();
}
} else {
// 等待锁释放,然后再次检查缓存
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return cache.get(key);
}
} catch (Exception e) {
e.printStackTrace();
}

return value;
}

private String queryDatabase(String key) {
// 模拟数据库查询
try {
Thread.sleep(1000); // 模拟查询延迟
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Value for " + key;
}

public static void main(String[] args) {
CacheService cacheService = new CacheService();

Runnable task = () -> {
String threadName = Thread.currentThread().getName();
String value = cacheService.getValue("mykey");
System.out.println(threadName + " got value: " + value);
};

// 启动多个线程同时访问缓存
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
}
}

缓存检查:
每个线程在 getValue 方法中首先尝试从缓存中获取值:

1
2
3
4
5

String value = cache.get(key);
if (value != null) {
return value;
}

获取锁:
如果缓存中没有数据,尝试获取锁:

1
2

if (lock.tryLock()) {

使用 tryLock 方法避免线程在锁上无限等待,可以更优雅地处理并发。

第二次检查缓存:
在获取到锁后,第二次检查缓存:

1
2
3
4
5
6
7
8

value = cache.get(key);
if (value == null) {
// 查询数据库
value = queryDatabase(key);
// 更新缓存
cache.put(key, value);
}

查询数据库和更新缓存:
如果缓存仍然没有数据,则查询数据库并更新缓存:

1
2
3

value = queryDatabase(key);
cache.put(key, value);

释放锁:
更新缓存后,确保锁被释放:

1
2

lock.unlock();

等待和重试:
如果没有获得锁,等待一段时间后再次检查缓存:

1
2
3
4
5
6
7
8

try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return cache.get(key);

bitmap/hyperloglog/GEO

需求痛点:上亿级别的数据的收集清洗统计和展现

存的进,取得快,多维度

大数据量的常见的四种统计

  1. 聚合统计

    alt text

  2. 排序统计

    设计一个最新的列表或者排行榜的需求,数据更新频繁,并且需要分页

    alt text

  3. 二值统计

    只有0和1 两种,使用bitmap

  4. 基数统计

    使用hyperloglog

hyperloglog

获取去重后的真实数据的个数

alt text

GEO

可以添加经纬度坐标,然后计算两个位置之间的距离,或者以半径为中心,查找附近的

添加坐标

alt text

计算距离

alt text

查找附近的

alt text

bitmap

可以统计日活,连续签到打卡,最近一周的活跃用户,统计指定用户一年之中的登录天数

缓存预热+缓存雪崩+缓存击穿+缓存穿透

  1. 缓存预热

    在系统启动的时候提前把常用的数据加载到缓存中,提高系统性能

  2. 缓存雪崩

    • redis主机挂了,全崩了或者是redis大量key同时过期

    • redis的key设置永远不过期

    • redis设置主从+哨兵

    • 开启redis持久化AOF/RDB快速恢复集群

    • 多缓预防雪崩

  3. 缓存穿透

    简单来说这个事情出现有两种情况:

    1. 黑客攻击了
    2. mysql中没有数据,redis的缓存中也没有数据,大量查询这个数据的请求进来了,就会给数据库很大的压力

    方案一:两次查询

    • 首先是第一次查询在redis缓存中查询数据,发现没有命中,然后去mysql中查询数据,发现也没有命中
    • 然后返回了一个null
    • 将这个key和默认的defaultNull值写入到redis中
    • 下一次的查询直接就在redis的缓存中查到了defaultNull,就不用查询数据库了,因为没有数

    方案二:布隆过滤器

    首先布隆过滤器是一个数据结构,可以用来判断一个元素是否在集合中

    在查询缓存和数据库之前,使用布隆过滤器检查请求的键是否可能存在。如果布隆过滤器认为键不存在,直接返回默认值。

  4. 缓存击穿

    大量的请求都查询一个key,这时候这个key正好失效了,大量的请求就会访问到数据库上

    方案一:

    一般业务部门知道哪些是热点的key,热点key就不设置过期时间了

    方案二:

    互斥锁,用互斥锁锁住,第一个拿到数据的就做缓存,后面的线程之间在缓存里面就取到了

alt text

reids为什么快

IO多路复用

IO多路复用要解决的问题是之前用一个进程来处理一个请求,这个太奢侈了,类似于一个学生配一个老师

IO:网络IO

多路:多个客户端连接,多个TCP

复用:一个进程处理多条连接,单线程就能实现多个客户端的连接

redis的IO多路复用为什么快

redis使用epoll来实现IO多路复用,把连接信息和事件放到队列,一起放到分派器,分派器再分发给事件处理器。这样一个进程来处理了大量的用户连接

redis的所有操作都是单线程的顺序执行,但是读写操作等待用户的输入输出都是杜塞的,IO操作一般不能直接返回,IO多路复用就是解决这个问题

同步和异步

这里扩展一下,之前看到的很有意思的一些名词解释
alt text

后续更新redis底层算法,redlock算法