637 常见的数据类型

  • String
  • Set
  • Hash
  • List
  • Zset (Sorted Set)

BitMap => 位图,考勤,或者xxx分配情况

HyperLogLog => 用户访问的独立用户数量

GEO => 地理

应用场景:

  • 缓存
  • 实时系统
  • 消息队列
  • 分布式锁
  • 计数器:页面访问量、点赞数、评论数

651 Redis 主从复制的实现原理

为什么需要主从复制

  • **提供故障恢复:**数据冗余&故障恢复,某个节点宕机了,但是其他节点还活着;
  • **负载均衡:**提供负载均衡,配合读写分离策略,主节点写操作,从节点提供读操作; ⭐
  • **高可用:**主从复制是Redis的高可用的基础,也是哨兵和集群实施的基础。

两种同步复制方式: 全量 & 增量

全量复制:分三阶段

  • 主节点发送SYNC命令与从节点进行连接,开始同步, 主从之间建立联系;
  • 主节点将当前数据生成RDB文件,发送给从节点;
  • 发送的期间,主节点将期间的所有写命令缓存到Replication buffer。最后发送Replication buffer的写命令给从节点。

增量复制 短暂失联后同步数据

主要有三个步骤:

  • 从服务器在恢复网络后,会发送 psync 命令给主服务器,此时的 psync 命令里的 offset 参数不是 -1;
  • 主服务器收到该命令后,然后用 CONTINUE 响应命令告诉从服务器接下来采用增量复制的方式同步数据;
  • 然后主服务将主从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。

那么关键的问题来了,主服务器怎么知道要将哪些增量数据发送给从服务器呢? 答案藏在这两个东西里:

  • repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据; (环形堆积缓冲区)

  • replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用master_repl_offset 来记录自己,「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。

  • ·repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;

  1. 保持连接与断线重连

    Redis中,主节点会和从节点保持长连接,以确保数据的持续同步;

    当连接断开后,重连,请求增量复制,避免全量复制带来的大量开销。

  2. 数据一致性和复制延迟

    由于网络延迟,主从之间会存在短暂的数据不一致。 ⭐对于数据一致性严格的任务,要求访问主节点。


652 Redis集群实现的原理

image-20250509190611210

  • Redis集群概念
  • 客户端怎么知道访问哪些槽,哈希槽
  • 实例没有数据怎么办?MOVED重定向
  • 各节点怎么通信?Gossip
  • 集群节点内故障转移

➡️通过多个Redis实例实现,每个实例存储部分数据,且这些数据是不重复的。

➡️具体为采用Hash Slot哈希槽,将键的空间划分为16384个槽。每个Redis实例只负责一部分槽。

客户端 => 任意Redis实例 => 数据是否在本机上,在返回,否则返回目标节点信息,客户端再路由到其他Redis中。

<= 将单个Redis的压力,分摊到多台Redis实例上。 大数据量存储导致的各种慢问题

特性

  • 实现数据分布式存储,对数据进行分片,将不同数据储存在不同节点中
  • 去中心化思想,无中心节点,访问任意一个即可。访问正确则响应数据,否则响应对应的节点信息,客户端再次访问。
  • 内置高可用性:分为N组,每组提供不同的数据缓存服务,每组中又有一个主节点和K个从节点(主提供读写,从提高读,并进行数据同步功能)

image-20250307175456538

布局:

  • 集群有多个master,每个master保存不同的数据(海量数据)
  • 每个master有多个slave (支持高并发读)
  • master之间通过ping检查彼此的健康度
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确的节点(路由规则)

存储与读取

  • 分片集群引入hash slot,一共有16384个槽
  • 不同实例处理不同的槽
  • 读写数据:根据有效部分计算hash值,对16384取余,得到插槽,寻找插槽所在实例。

传统哈希

1hash(key)%N  N:服务器数量 N一旦变了大部分数据都需要重新映射到新的服务器上

一致性哈希❌❌❌

635 Redis为什么快

存储方式、线程模型、IO模型、数据结构

  • 基于内存

  • 单线程事件驱动(避免线程切换开销,避免锁竞争,数据一致性) 结合 IO多路复用(单线程同时处理多个客户端连接)

  • 高效数据结构 => (String,List, Set)

  • 多线程引入 => 网络处理并发请求,减少网络IO等待影响 (网络IO可能成为性能瓶颈)

  • Redis主线程很快,但是网络IO处理不一定更得上它的速度,可能成为累赘。

  • 使用多个IO进程加快网络IO速度(数据序列号&反序列化,客户端请求的解析…)

image-20250307181554873

636 Redis中的多线程

  • 基于内存,主线程很快。 但IO拖了后腿(性能瓶颈)。

638 跳表

多层链表实现,底层链表保存所有元素,每层链表都是下一层的子集 (有序链表的基础上添加多级索引)

特性

  • 快查找某个元素 & 范围查询 复杂度O(logn)
1Level 3:       1 ---------------> 9 ----------------> 20
2Level 2:       1 ------> 5 -----> 9 ----> 13 -----> 20
3Level 1:       1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15 -> 20
4Level 0:       1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 16 -> 20

从高层开始往下找

插入时候呢 插入的随机层级:概率函数决定从下(level-0)到上(level-n) 概率为 0.25(n)

跳表结构

1Class SkipNode{
2    int value;
3    SkipNode[] forward;// 指向不同层级的后继, 下一个level节点
4    
5    public SkipNode(int value, int level) {
6        this.value = value;
7        this.forward = new SkipNode[level + 1]; // 0 到 level 层 每一个节点,动态的数据长度
8    }
9}
 1class SkipList {
 2    private static final int MAX_LEVEL = 16; // 最大层数
 3    private int level; // 当前层数
 4    private SkipListNode header; // 头节点
 5    private Random random; // 随机数生成器,用于决定节点层数
 6	
 7    // 初始化头节点,其中有[0,...,MAX_LEVEL]的节点
 8    public SkipList() {
 9        this.level = 0;
10        this.header = new SkipListNode(Integer.MIN_VALUE, MAX_LEVEL);
11        this.random = new Random();
12    }
13    
14    // 插入节点
15    public void insert(int value) {
16        SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1]; // 记录每层需要更新的节点位置
17        SkipListNode current = header;
18
19        // 从最高层开始查找插入位置
20        for (int i = level; i >= 0; i--) {
21            // 找到插入节点的位置
22            while (current.forward[i] != null && current.forward[i].value < value) {
23                current = current.forward[i];
24            }
25            update[i] = current; // 记录插入节点的位置
26        }
27
28        // 随机生成新节点的层数
29        int newLevel = randomLevel();
30
31        // 如果新节点的层数大于当前层数,更新 update 数组和当前层数
32        if (newLevel > level) {
33            for (int i = level + 1; i <= newLevel; i++) {
34                update[i] = header;
35            }
36            level = newLevel;
37        }
38
39        // 创建新节点
40        SkipListNode newNode = new SkipListNode(value, newLevel);
41
42        // 更新每层的指针
43        for (int i = 0; i <= newLevel; i++) {
44            newNode.forward[i] = update[i].forward[i];
45            update[i].forward[i] = newNode;
46        }
47
48        System.out.println("Inserted: " + value);
49    }
50
51    
52}
13 []=>^
22 []  =>  [] =>^
31 []=>[]=>[] =>^
40 []=>[]=>[] =>^
5  -1   3   6
6    
7update 记录每一层的插入位置
8SkipListNode newNode = new SkipListNode(value, newLevel); 每个节点数组长度为newLevel+1

639 Redis Hash

键值对集合

ObjKey => {k1:v1, k2:v2}

642 Redis数据过期删除策略

  • 定时删除 => 定时任务,多少时间检查,删除过期的键
  • 惰性删除 => 访问时,判断,过期了才删

内存回收机制

  • 当内存使用达到限制了,主动删除不常用的数据。 LRU算法

643 Redis内存淘汰策略

设置了过期时间的数据 淘汰策略

  • random
  • ttl
  • lru 最近最少使用 least recent used
  • lfu 最不常用 least frequency used

644 Redis Lua脚本

核心

  • 原子性,避免并发修改带来的安全问题
  • 减少网络往返次数
  • 复杂操作

Lua本身不具备原子性,但是Redis线程是单线程,因此Lua任务会一同执行,其他任务会被阻塞

646 Redis BigKey问题,以及解决

Big Memory Key,key对应的value超级大

  • 内存分配不均,在集群模式下,如何某个实例的大key过多,负担不均衡
  • 客户端超时
  • 阻塞其他任务

解决

开发方面

  • 开发时,数据压缩
  • 大化小,拆成几份
  • 选择合适的数据结构存储

业务方面

  • 存储必要信息

数据分布方面

  • 大Key拆分成小key,分散到不同redis实例中

647 如何解决redis中的热点key问题

问题:某个key被频繁访问,导致redis压力过大

  • 热点key拆分:将多个热点数据分散到多个key中,例如通过前缀,使不同请求分散到多个key中,且分布在多实例中,避免集中访问单一key。

    • 全量拷贝,key:datainfo => key:datainfo_1, key:datainfo_2, key:datainfo_3
    • Key拆分,每个key只存一部分

    例如:热点库存key => stock:product_123,这个key被大量访问

    => 对其进行拆分 4份

    1stock:product_123:1
    2stock:product_123:2
    3stock:product_123:3
    4stock:product_123:4
    5
    6# 业务层
    7int shardIndex = (productId.hashCode() & Integer.MAX_VALUE) % shardCount;
    8String shardKey = "stock:product_123:" + shardIndex;
    

    将请求拆分成多个key,分摊压力

    ⭐数据一致性:需要同步所有相关的key

  • 多级缓存:在Redis前,增加其他缓存层,如(CDN,本地缓存),分担redis的压力; CDN(Content Delivery Network 内容分发网络);

  • 读写分离:通过Redis主从复制,将读请求分发到多个从节点,分摊压力;

  • 限流和降级:应用限流策略,减少对redis的请求,必要时回传空值或降级数据。

648 Redis的持久化机制

  • RDB(redis database)快照
    • 通过生成某一时刻的数据快照来实现持久化,间隔一定时间
    • 二进制文件,数据紧凑,恢复数据快
    • 写时复制技术
  • AOP(Append only file)日志
    • AOP通过将每个写操作追加到日志中实现持久化,以便根据操作日志进行恢复,重放
    • 恢复精确,但文件体积大
    • 操作记录什么时候写回磁盘
      • 立即,每个操作 =>
      • 间隔, 间隔一定时间=> 写AOF
      • 缓冲区满,再写AOF
  • 混合RDB+AOP
    • 利用RDB文件快和AOF的精确
    • 备份的时候,先生成RDB,再将新增的写操作加到AOF文件后面

649 Redis在生成RDB快照时,如何处理请求

写时复制技术

  • RDB快照并不会复制数据,而是复制页表(相当于指针)
  • 当Redis处理写请求时,会复制对应的页数据,这样RDB快照和当前数据的页表部分指向将变化了,进行错开.
  • RDB快照在利用fork子进程存储快照

650 Redis哨兵机制 sentinel ˈsentɪn(ə)l

image-20250509184509746

=> 系统能够在长时间运行中保持较高的可用性稳定性,即使在出现故障时仍然能快速恢复,确保业务不中断

一种高可用性(稳健)解决方案,用于监控Redis主从集群,自动完成主从切换,以及故障自动恢复和通知 (主从节点故障转移 - 主节点挂了自动推举出新的主节点)

  • 监控:哨兵会不断的Ping redis主节点和从节点,定时Ping Redis实例,检查存活状态
  • 自动故障转移:主节点宕机了,会选择一个从节点晋升为新的主节点,并通知客户端
  • 通知,向管理员或其他服务发送通知,以便快速处理redis实例的变化

主观下线和客观下线

多个哨兵可以避免单个哨兵因网络问题的主观下线 - 哨兵是以哨兵集群的方式存在的

image-20250509184704196

1) sentinel每隔1s Ping所有节点,当超过一段时间没收到对于节点的Pong,主观认为下线。

2)客观下线(主节点而已):如果主节点宕机了,它向其他哨兵发起投票,只有当下线的投票数大于一半的时候,才认为主节点宕机了。

哪个哨兵负责主从复制转移呢?

判断redis主节点客观下线的哨兵节点均为候选节点,然后发送投票信息给其他哨兵节点,进行投票。 之后得到哨兵Leader来负责故障转移;

一、Redis新主节点选举 - 优先级、复制进度、ID 号

  • 优先级, 通过配置文件, slave_priority

  • 优先级相同,则看同步状态offset(数据同步,复制偏移),数据越一致,越可能成为主节点

  • 同步状态一致,则比较ID号

二、选好master redis后,sentinel会把其他redis实例变为master redis的slave

三、将新主节点的IP和端口通知给客户端


653. Redis集群脑裂问题

在网络分区的情况下,可能导致同一个集群出现多个主节点。

分布式系统中,由于网络分区问题导致系统多个节点误认为自己是主节点,导致多个主节点提供写入服务,导致数据不一致。

image-20250319220151017 image-20250319220208573

🏷️避免脑裂问题:

  • min-slaves-to-write】设置主节点至少有指定的从节点的情况下才执行写操作。
  • min-slaves-max-lag】设置从节点的最大延迟,如果从节点的延迟超过这个值,则不计入min-slaves-to-write

这样当脑裂后,某个主节点的从节点数量不够或者延迟较大,就无法写入,避免多个主节点写入造成的数据不一致。 从节点与主节点延迟大,则不计数为从节点【并不能完全解决】


655 实现分布式锁

set ex nx (set expire_time not-exists) 命令 + lua脚本实现

  • 加锁:SET lock_key uniqueValue EX expire_time NX
  • 解锁如下
1if redis.call("GET",KEYS[1]) == ARGV[1]   # 避免别人删了这把锁
2then
3    return redis.call("DEL",KEYS[1])
4else
5    return 0
6end
image-20250307223835242

656 分布式锁在未完成业务时,过期了怎么办

image-20250307224018962

=> 逻辑途中,给它续期

扩展知识 - 看门狗机制

抢到锁之后,启动后台定时任务,定时向redis进行锁的续期。比如每过1/3锁的过期时间,给锁续期

业务完成,再把这个后台定时任务给结束

  • 并且分布式锁需要满足谁上锁谁解锁
  • 释放锁时,需要1.检查锁是不是自己上的,再释放锁. 两步=> Lua脚本

654 Redis 订阅&发布

1                     => Subscriber 1
2publisher => channel => Subscriber 2
3                     => Subscriber 3

消息的订阅和推送 - 阻塞式消息拉取

SUBSCRIBE channel
PUBLISH channel message
UNSUBSCRIBE channel

Redis Red Lock

主从+哨兵模式下呢,进行主从切换时,从节点可能没有完全复制主节点信息。锁没有同步过来。

资源的竞争

=> 至少一半的Redis节点拿到了锁才算上锁成功。 性能比较差。

658 Redis 实现分布式锁可能遇到的问题有哪些?

分布式锁需要满足的要求

  • 锁的互斥: Redis setnx+redis单线程
  • 可重入性:一个线程可以重复拿到同一把锁
  • 锁的性能: 需要基于内存

会出现问题:

  • 锁误解 => 需要确保锁自己自己解锁, 或者过期(线程ID判断下)

  • 锁的有效时间 => Watchdog机制,给锁定期续时间

  • 单点故障问题 => RedLock =>

    • 需要保证一半以上的节点加锁成功才算拿到这把锁
  • 可重入锁

 1-- tryLock.lua
 2-- KEYS[1] 锁的Key    
 3-- ARGV[1] 当前线程标识
 4-- ARGV[2] 锁的过期时间
 5    
 6local lockValue = redis.call('get', KEYS[1]) 
 7if lockValue == false then
 8    // 锁不存在
 9    redis.call('setex', KEYS[1], ARGV[2], ARGV[2])
10    return true
11else
12    // 锁存在
13    local parts = {}
14	local index = 0
15    for match in (localValue .. ":"):gmatch("(.-):") do
16    	parts[index] = match
17        index = index + 1
18    end
19    if parts[0] == ARGV[1] then
20    	-- 锁由当前线程所有,重入次数+1
21        local count = tonumber(parts[1]) + 1
22        redis.call('setex', KEYS[1], ARGV[2], ARGV[1] .. ":" .. count)          
23        return true
24     end
25end
26return false

659 Redis中的缓存击穿、缓存穿透和缓存雪崩

概念

  • 缓存击穿: 某个热点Key数据在缓存中失效,导致大量的请求直接访问数据库。 瞬间的高并发,可能导致数据库崩溃; – 热点过期\删除
  • 缓存穿透: 指查询一个不存在的数据,缓存中没有存储,直接二次映射到数据库中查询,造成数据库负担;
  • 缓存雪崩:多个缓存数据在同一时间过期,导致大量请求同时访问数据库,从而造成数据库瞬间负载激增。 – 批量过期

解决

  • 缓存击穿:

    • 1 加互斥锁,保证同一请求只有一个请求来重新构建缓存,其他线程等待。

    • 2 热点数据永不过期

    • 3 双重检查

       1String data = redisTemplate.opsForValue().get("hot_key")
       2if(data == null) { // 数据没缓存
       3    synchronized(this) { // 加速,仅一个线程取构建缓存
       4        String data = redisTemplate.opsForValue().get("hot_key") // 这里再次判断,这样其他进入同步块的就不用重新访问DB构建缓存了
       5        if(data == null){
       6            data = queryFromDB();
       7            redisTemplate.opsForValue().set("hot_key", data, 30, TimeUnit.MINUTES);
       8        }
       9    }
      10}
      
  • 缓存穿透(避免二次路由到数据库,拦截它):

    • 使用布隆过滤器,过滤掉不存在的请求,避免直接访问数据库; hash(where condition)

      实现// 快速判断一个元素是否在BitMap中,将字符串用多个Hash函数映射到不同的二进制位置,将对齐位置设置为1;当查询的时候,需要所有位置都置1,那么该数据才可能存在。因为不会重置为0;

    • 对查询结果进行缓存,即使不存在的数据,也可以换成个标识(空数据,较短的过期时间),减少对数据库的请求

  • 缓存雪崩(多个过期)

    • 数据预热,提前将热门的数据加载到缓存中,避免高并发出现大量的数据库访问。
    • 采用随机分布的方式设置缓存失效时间,避免多个缓存数据同时过期(批量过期);添加随机值,进行偏移
    • 双缓存策略,将数据同时储存在两层缓存中。
      • 在Redis之前,添加一层本地缓存,减少对Redis的依赖。

​ ⭐Caffeine高性能缓存库 LRU淘汰策略

 1@Service
 2public class CacheService {
 3    @Resource
 4    private Cache<String, String> localCache;
 5
 6    public void put(String key, String value) {
 7        localCache.put(key, value);
 8    }
 9
10    public String get(String key) {
11        return localCache.getIfPresent(key);
12    }
13}
14
15@Configuration
16@EnableCaching
17public class CaffeineCacheConfig {
18
19    @Bean
20    public com.github.benmanes.caffeine.cache.Cache<String, String> localCache() {
21        return Caffeine.newBuilder()
22                .maximumSize(1000)  // 最大缓存条目
23                .expireAfterWrite(10, TimeUnit.MINUTES)  // 10 分钟自动过期
24                .build();
25    }
26    
27    @Bean
28    public CacheManager cacheManager() {
29        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
30        cacheManager.setCaffeine(Caffeine.newBuilder()
31                .expireAfterWrite(10, TimeUnit.MINUTES)
32                .maximumSize(1000));
33        return cacheManager;
34    }
35}
36
37// # ----------- 用法
38@Cacheable(value = "users", key = "#id")
39public String getUserById(String id) {
40    System.out.println("查询数据库:" + id);
41    return "User_" + id;
42}   

本地缓存+Redis (多级缓存)

 1public String getData(String key) {
 2    // 1. 先查本地缓存
 3    String value = localCache.getIfPresent(key);
 4    if (value != null) return value;
 5
 6    // 2. 查 Redis
 7    value = redisTemplate.opsForValue().get(key);
 8    if (value != null) {
 9        // 回写本地缓存
10        localCache.put(key, value);
11        return value;
12    }
13
14    // 3. 查询数据库(模拟)
15    value = queryFromDB(key);
16
17    // 4. 回写 Redis 和本地缓存
18    redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
19    localCache.put(key, value);
20    return value;
21}

660 Redis与MySQL的一致性策略

主要就是网络问题,和请求并发问题造成的不一致性

⭐ 让redis和mysql数据一样⭐

  • 先更新Redis,再更新MySQL ❌不推荐
image-20250308135639431
  • 先更新MySQL,再更新Redis ❌不推荐
image-20250308140251136
  • 先删除Redis,再更新MySQL,最后写回Redis ❌不推荐
image-20250308140614242
  • 先更新MySQL,再删除Redis,等请求重新缓存(惰性) ✔️推荐
  • 缓存双删除策略。更新MySQL之前,删除一次Redis;更新完MySQL后,再进行一次延迟删除 ✔️推荐
image-20250308140849778

数据库没问题,但是缓存有问题,等待一段实践

  • 使用Binlog异步更新缓存,监听数据库的binlog变化,通过异步方式更新Redis缓存 ✔️推荐

# ==========================

GPT版本:

  • 📌方案一: 延迟双删(最终一致性),核心思路如下:
    • 删除Redis缓存
    • 更新MySQL数据
    • 等待一小会时间
    • 再次删除Redis缓存,防止并发请求写回脏数据
 1// 1. 先删除 Redis
 2redisTemplate.delete(key);
 3
 4// 2. 更新 MySQL
 5databaseService.updateDataInMySQL(key, newValue);
 6
 7// 3. 延时再删除一次 Redis(解决并发问题)
 8new Thread(() -> {
 9    try {
10        TimeUnit.MILLISECONDS.sleep(500);
11        redisTemplate.delete(key);
12    } catch (InterruptedException e) {
13        e.printStackTrace();
14    }
15}).start();

✅ 适用场景

✔ 适用于读多写少的场景(如商品详情页) ✔ 不影响数据库写入性能 ✔ 代码简单,容易实现

🔴 缺点

❌ 存在 500ms 的数据不一致窗口 ❌ 不能保证强一致性

📌方案2:异步消息队列(最终一致性)。核心思路如下:

  • 更新MySQL数据
  • 发送消息到RabbitMQ
  • 消息消费者监听更新,删除Redis缓存
 1public void updateData(String key, String newValue) {
 2    // 1. 更新 MySQL
 3    databaseService.updateDataInMySQL(key, newValue);
 4
 5    // 2. 发送消息到 MQ,通知消费者删除 Redis
 6    rabbitTemplate.convertAndSend("cache_update_queue", key);
 7}
 8
 9@RabbitListener(queues = "cache_update_queue")
10public void processCacheUpdate(String key) {
11    redisTemplate.delete(key);
12}

✅ 适用场景

✔ 适用于高并发场景 ✔ 避免了 Redis 和 MySQL 直接耦合 ✔ 保证最终一致性

🔴 缺点

❌ 需要额外的 MQ 服务(Kafka、RabbitMQ) ❌ 增加系统复杂度

📌方案3:监听MySQL Binlog(强一致性)。

✅核心思路如下:

  • 监听MySQL Binlog日志
  • 实时解析SQL变更
  • 删除Redis缓存
  • 保证Redis和MySQL强一致性

✅实现方式

  • Canal 阿里巴巴开源
1public void onMessage(String binlogData) {
2    String changedKey = parseBinlog(binlogData);
3    redisTemplate.delete(changedKey);
4}

✅ 适用场景

✔ 适用于高一致性要求的业务(如金融、电商支付) ✔ 高吞吐,低延迟,实时清理缓存 ✔ 适用于分布式系统

🔴 缺点

❌ 依赖 Canal,运维成本较高 ❌ 需要解析 Binlog,代码实现复杂

评论:

image-20250308143904146


661 Redis String 类型底层实现 ❌

Redis String类型底层实现主要基于 SDS(Simple dynamic string)结构,并结合编码方式进行优化存储

1struct __attribute__ ((__packed__)) sdshdr64 {
2    uint64_t len; /* used */
3    uint64_t alloc; /* excluding the header and null terminator */
4    unsigned char flags; /* 3 lsb of type, 5 unused bits */
5    char buf[];
6};

flags: SDS类型,SDS有5个类型 => sdshdr5, sdshdr8 sdshdr16 sdshdr32 sdshdr64

// simple dynamic string header => sdshdr


662 如何使用Redis快速实现排行榜

[(userInfo, liked_star), …, ]

  • 利用Sorted Set(跳表实现) 存储分数和成员

663 如何利用Redis实现布隆过滤器

利用BitMap: (setBit, getBit)

 1// 添加元素到布隆过滤器
 2public void add(String value)
 3    for(SimpleHash hashFunc : hashFuncs) {
 4        xxx.setbit(BLOOM_FILTER_KEY, hashFUnc(Value))
 5    }
 6
 7// 检查某个元素是否存在于布隆过滤器中
 8public boolean mightContain(String value) {
 9    for(SimpleHash hashFunc : hashFuncs) {
10        if(xxx.getBit(BLOOM_FILTER_KEY, hashFunc(value)) !=1) {
11            return false;
12        }
13}    

布隆过滤器的适用场景

  • 爬虫:URL去重

  • 黑名单:判断

  • 分布式系统:判断数据是否在某个节点上

  • 推荐系统:判断用户是否看过某个内容

664 如何利用Redis统计大量用户唯一访问量

=> HyperLogLog

  • PFADD key element => ADD HyperLogLog中
  • PFCOUNT key => return unique number

665 Redis中的Geo数据结构

  • GEOADD key longitude latitude member
  • GEODIST key member1 member2
  • GEORADIUS key longitude latitude radius unit;

668 Redis 性能瓶颈

扩容内存、读写分离、集群模式、多级缓存(本地、redis、mysql)

885 Redis Cluster模式 和Sentinel模式

集群与哨兵模式

  • 集群 => 数据切片,每个集群负责一部分数据, 集群内又分为主从模式;

  • 哨兵:高可用性,当master宕机,根据规则选择最优的slave并晋升为新的master

1.Cluster集群模式:集群模式用于对数据进行分片,主要用于解决大数据、高吞吐量的场景。将数据自动分不到多个Redis实例上,支持自动故障转移(如果某个实例失效,集群会自动重写配置和平衡,不需要手动进行调整,因为内置了哨兵逻辑) 2.Sentinel哨兵模式: 哨兵模式用于保证主从节点的高可用,读写分离场景。如果主节点宕机,哨兵会将从节点升为主节点.

6305 Redisson分布式锁的原理

Redisson是基于Redis+lua脚本实现的分布式锁, 使用redis的原子操作来确保多线程…, 只能有一个线程能够获取到锁,实现互斥。

主要包括:

  • 获取锁:(setNX + 过期时间)
  • 自动续期:(Watchdog 机制)
  • 可重入性:(线程ID计数器,当值为0表示才真正的删除锁)
  • 释放锁:(Lua脚本保证原子性)
 1// ### get lock
 2RLock lock = redisson.getLock("lock:key")  
 3// => SET mylock thread-id NX PX 30s;  thread-id保证可重入    
 4lock.lock()
 5    
 6// ### 自动续期 watchdog
 7// 如果线程未主动释放锁,redisson则每隔10s续期30s
 8PEXPIRE lock:key 30s
 9    
10// ### 可重入性
11// 第一次加锁
12lock.lock();
13
14// 同一线程再次加锁
15lock.lock();
16
17// 释放锁
18lock.unlock();
19lock.unlock(); // 只有 count=0 时才真正释放

分布式锁最佳实践

 1RLock lock = redisson.getLock("myLock");
 2
 3try {
 4    // 尝试获取锁,最多等待 5 秒,10 秒后自动释放(防止死锁)
 5    if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
 6        try {
 7            // 执行业务逻辑
 8            // ...
 9        } finally {
10            lock.unlock(); // 释放锁
11        }
12    } else {
13        System.out.println("获取锁失败");
14    }
15} catch (InterruptedException e) {
16    e.printStackTrace();
17}