Redis

Redis见解

1.Redis 可以做什么?

1、记录帖子的点赞数、评论数和点击数 (hash)。  
2、记录用户的帖子 ID 列表 (排序),便于快速显示用户的帖子列表 (zset)。  
3、记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)。  
4、记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)。  
5、缓存近期热帖内容 (帖子内容空间占用比较大),减少数据库压力 (hash)。  
6、记录帖子的相关文章 ID,根据内容推荐相关帖子 (list)。  
7、如果帖子 ID 是整数自增的,可以使用 Redis 来分配帖子 ID(计数器)。  
8、收藏集和帖子之间的关系 (zset)。  
9、记录热榜帖子 ID 列表,总热榜和分类热榜 (zset)。  
10、缓存用户行为历史,进行恶意行为过滤 (zset,hash)。

2.Redis 基础数据结构

Redis 有 5 种基础数据结构,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)

1.string (字符串)

Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M

键值对:

> set name codehole  
OK  
> get name  
"codehole"  
> exists name  
(integer) 1  
> del name  
(integer) 1  > 
get name  (nil)

批量键值对 : 可以批量对多个字符串进行读写,节省网络耗时开销

> set name1 codehole  
OK  
> set name2 holycoder  
OK  
> mget name1 name2 name3 //返回一个列表  
1) "codehole"  2) "holycoder"  3) (nil)  
> mset name1 boy name2 girl name3 unknown  
> mget name1 name2 name3  
1) "boy"  
2) "girl"  
3) "unknown" 

过期和 set 命令扩展: 可以对 key 设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。

> set name codehole  
> get name "codehole"  
> expire name 5 // 5s 后过期  ... # wait for 5s  
> get name  
(nil)  
> setex name 5 codehole // 5s 后过期,等价于 set+expire  
> get name  "codehole"  ... # wait for 5s  
> get name  
(nil)  
> setnx name codehole // 如果 name 不存在就执行 set 创建  (integer) 1  
> get name  
"codehole"  
> setnx name holycoder  
(integer) 0 // 因为 name 已经存在,所以 set 创建不成功  
> get name  "codehole" // 没有改变 

计数 如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值,超过了这个值,Redis 会报错。

> set age 30  
OK  
> incr age  
(integer) 31  
> incrby age 5  
(integer) 36  
> incrby age -5  
(integer) 31 
> set codehole 9223372036854775807  // Long.Max  OK  > incr codehole 

(error) ERR increment or decrement would overflow  字符串是由多个字节组成,每个字节又是由 8 个 bit 组成,如此便可以将一个字符串看
成很多 bit 的组合,这便是 bitmap「位图」数据结构,位图的具体使用会放到后面的章节来讲。 

2.list (列表)

Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n),这点让人非常意外。
当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。

Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。

右边进左边出:队列:

> rpush books python java golang  
(integer) 3  
> llen books  
(integer) 3  
> lpop books  
"python"  
> lpop books  
"java"  
> lpop books  
"golang"  
> lpop books 
(nil) 

右边进右边出:栈:

> rpush books python java golang  
(integer) 3  
> rpop books  
"golang"  
> rpop books  
"java"  
> rpop books  
"python"  
> rpop books 
(nil) 

慢操作

lindex 相当于 Java 链表的 get(int index)方法,它需要对链表进行遍历,性能随着参数index 增大而变差。 ltrim 和字面上的含义不太一样,个人觉得它叫 lretain(保留) 更合适一些,因为 ltrim 跟的两个参数 start_index 和 end_index 定义了一个区间,在这个区间内的值,ltrim 要保留,区间之外统统砍掉。我们可以通过 ltrim 来实现一个定长的链表,这一点非常有用。index 可以为负数,index=-1 表示倒数第一个元素,同样 index=-2 表示倒数第二个元素。

![redis-list(quicklist)](F:\工作\学习\img\redis-list(quicklist).jpg)> rpush books python java golang  
(integer) 3  
> lindex books 1 // O(n) 慎用  "java"  
> lrange books 0 -1 // 获取所有元素,O(n) 慎用  
1) "python"  
2) "java"  
3) "golang"  
> ltrim books 1 -1 // O(n) 慎用  
OK  
> lrange books 0 -1  
1) "java"  
2) "golang"  
> ltrim books 1 0 // 这其实是清空了整个列表,因为区间范围长度为负  
OK  
> llen books  
(integer) 0 

快速列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QIRyvtbn-1624523996471)(img\redis-list(quicklist)].jpg)

如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 linkedlist,而是称之为快速链表 quicklist 的一个结构。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。

比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next 。所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个
ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余

3.hash (字典)

Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vAbCjlGH-1624523996473)(img\redis-hash.jpg)]

hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 可以对用户结构中的每个字段单独存储。

这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。

hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡

同字符串一样,hash 结构中的单个子 key 也可以进行计数,它对应的指令是 hincrby,
和 incr 使用基本一样。

//老钱又老了一岁  
> hincrby user-laoqian age 1  
(integer) 30  

4.set (集合)

Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL。
当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。 set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。

> sadd books python  
(integer) 1  
> sadd bookspython // 重复  
(integer) 0  
> sadd books java golang  
(integer) 2  
> smembers books // 注意顺序,和插入的并不一致,因为 set 是无序的  
1) "java"  
2) "python"  
3) "golang"  
> sismember books java // 查询某个 value 是否存在,相当于 contains(o)  
(integer) 1  
> sismember books rust  
(integer) 0  
> scard books // 获取长度相当于 count() 
(integer) 3  
> spop books # 弹出一个  
"java"

5.zset (有序列表)

zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。

它的内部实现用的是一种叫着「跳跃列表」的数据结构。 zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。

zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。
zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们
可以对成绩按分数进行排序就可以得到他的名次。

> zadd books 9.0 "think in java"  
(integer) 1  
> zadd books 8.9 "java concurrency" 
(integer) 1  
> zadd books 8.6 "java cookbook"  
(integer) 1  
> zrange books 0 -1 // 按 score 排序列出,参数区间为排名范围  
1) "java cookbook"  
2) "java concurrency"  
3) "think in java"  
> zrevrange books 0 -1 // 按 score 逆序列出,参数区间为排名范围  
1) "think in java"  
2) "java concurrency"  
3) "java cookbook"  
> zcard books  // 相当于 count()  
(integer) 3  
> zscore books "java concurrency" // 获取指定 value 的 score  
"8.9000000000000004" // 内部 score 使用 double 类型进行存储,所以存在小数点精度问题  
> zrank books "java concurrency" // 排名  
(integer) 1  
> zrangebyscore books 0 8.91 // 根据分值区间遍历 zset  
1) "java cookbook"  
2) "java concurrency"  
> zrangebyscore books -inf 8.91 withscores // 根据分值区间 (-∞, 8.91] 遍历 zset,同时返 回分值。inf 代表 infinite,无穷大的意思。  
1) "java cookbook"  
2) "8.5999999999999996"  
3) "java concurrency"  
4) "8.9000000000000004"  
> zrem books "java concurrency" // 删除 value  
(integer) 1 
> zrange books 0 -1  
1) "java cookbook"  
2) "think in java" 

跳跃列表

zset 内部的排序功能是通过「跳跃列表」数据结构来实现的,它的结构非常特殊,也比较复杂

因为 zset 要支持随机的插入和删除,所以它不好使用数组来表示。我们先看一个普通的链表结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hEqtciTD-1624523996474)(img\redis-zset链表.jpg)]

我们需要这个链表按照 score 值进行排序。这意味着当有新元素需要插入时,要定位到特定位置的插入点,这样才可以继续保证链表是有序的。通常我们会通过二分查找来找到插入点,但是二分查找的对象必须是数组,只有数组才可以支持快速位置定位,链表做不到 这里就用到了类似层级制

跳跃列表就是类似于这种层级制,最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。然后在这些代表里再挑出二级代表,再串起来。最终就形成了金字塔结构。

想想你老家在世界地图中的位置:亚洲->中国->安徽省->安庆市->枞阳县->汤沟镇->田间村->xxxx 号,也是这样一个类似的结构。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7dFz9Wu6-1624523996476)(img\redis-zset(跳跃链表)].jpg)「跳跃列表」之所以「跳跃」,是因为内部的元素可能「身兼数职」,比如上图中间的这个元素,同时处于 L0、L1 和 L2 层,可以快速在不同层次之间进行「跳跃」。

定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。你也许会问,那新插入的元素如何才有机会「身兼数职」呢? 跳跃列表采取一个随机策略来决定新元素可以兼职到第几层。

首先 L0 层肯定是 100% 了,L1 层只有 50% 的概率,L2 层只有 25% 的概率,L3 层只有 12.5% 的概率,一直随机到最顶层 L31 层。绝大多数元素都过不了几层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。
这还挺公平的,能不能进入中央不是靠拼爹,而是看运气

容器型数据结构的通用规则

list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
1、create if not exists
如果容器不存在,那就创建一个,再进行操作。

比如 rpush 操作刚开始是没有列表的,Redis 就会自动创建一个,然后再 rpush 进去新元素。
2、drop if no elements
如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一
个元素,列表就消失了

过期时间:

Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期,而不是其中的某个子 key。
还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了 set 方法修改了它,它的过期时间会消失

127.0.0.1:6379> set codehole yoyo  
OK  
127.0.0.1:6379> expire codehole 600  
(integer) 1  
127.0.0.1:6379> ttl codehole  
(integer) 597  
127.0.0.1:6379> set codehole yoyo  
OK  
127.0.0.1:6379> ttl codehole 
(integer) -1 

3.实际应用

1.分布式锁

1.为什么要用到分布式锁

分布式应用进行逻辑处理时经常会遇到并发问题
比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的

这个时候就要使用到分布式锁来限制程序的并发执行。Redis 分布式锁使用非常广泛

2.什么是原子操作

所谓原子操作是指不会被线程调度机制打断的操作;

这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换

3.什么是分布式锁

例如: 分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

占坑一般是使用 
setnx(set if not exists)  指令,
只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。 
// 这里的冒号:就是一个普通的字符,没特别含义,它可以是任意其它字符,不要误解 c
> setnx lock:codehole true 
OK 
... do something critical ... 
> del lock:codehole 
(integer) 1 
4.死锁 -> 解决办法

但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
于是我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。

> setnx lock:codehole true 
OK 
> expire lock:codehole 5 ... 
do something critical ... 
> del lock:codehole 
(integer) 1

以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。

Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象

这个指令就是 setnx 和 expire 组合在一起的原子指令,它就是分布式锁的奥义所在

 > set lock:codehole true ex 5 nx 
 OK ... do something critical ... 
 > del lock:codehole
5.超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。

如果此时A的锁过期了,B就拿到了锁,然后此时A的逻辑执行完毕,释放了此锁(正被B拥有),C就会在B正在执行的时候拿到此锁

解决办法:

更加安全的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key

注意:

匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于 delifequals 这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。

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

参考:

https://blog.csdn.net/twt936457991/article/details/90181855?utm_term=%E5%88%86%E5%B8%83%E5%BC%8Fredis%E9%94%81%E6%B3%A8%E6%84%8F%E9%97%AE%E9%A2%98&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allsobaiduweb~default-0-90181855&spm=3001.4430

@RestController
@RequestMapping("/redis")
public class RedisController {
    private static Logger logger = LoggerFactory.getLogger(RedisController.class);

    //总库存
    private long nKuCuen = 10;
    //商品key名字
    private String shangpingKey = "computer_key";
    //获取锁的超时时间 5秒
    private int timeout = 5 * 1000;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/qiangdan")
    public List<String> qiangdan() {
        //抢到商品的用户
        List<String> shopUsers = new ArrayList<>();
        //构造很多用户 1w用户
        List<String> users = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            users.add("黄牛-" + i);
        }

        //初始化库存
        nKuCuen = 10;
        //模拟开抢  并行流
        users.parallelStream().forEach(user -> {
            String shopUser = qiang(user);
            if (!StringUtils.isEmpty(shopUser)) {
                shopUsers.add(shopUser);
            }
        });
        return shopUsers;
    }

    /**
     * 模拟抢单动作
     *
     * @param user
     * @return
     */
    private String qiang(String user) {
        //开始时间
        long startTiem = System.currentTimeMillis();
        while ((startTiem + timeout) >= System.currentTimeMillis()) {
            //判断未抢成功的用户,timeout秒内继续获取锁
            if (nKuCuen <= 0) {
                //库寸为 0
                System.out.println(user + "---没了" );
                break;
            }
            //获取锁
            Boolean success = redisTemplate.opsForValue().setIfAbsent(shangpingKey, user, 60, TimeUnit.SECONDS);
            if (success) {
                //获取到锁
                logger.info("用户{}拿到锁...", user);
                //商品是否剩余
                if (nKuCuen <= 0) {
                    break;
                }
                //模拟生成订单耗时操作,方便查看:神牛-50 多次获取锁记录
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //请购成功库存减一
                nKuCuen -= 1;
                //删除锁
                Boolean isDel = unLock(shangpingKey, user);
                if (isDel) {
                    //抢单成功跳出
                    logger.info("用户{}抢单成功跳出...所剩库存:{}", user, nKuCuen);
                    return user + "抢单成功,所剩库存:" + nKuCuen;
                }
            }
        }
        return null;
    }

    /*
    LUA脚本 根据key和value(UUID是随机产生的 也就是一个线程对应一个value)判断是否存在  存在就删除
    主要是为了解决 超时问题 :
    如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。
    如果此时A的锁过期了,B就拿到了锁,然后此时A的逻辑执行完毕,释放(删除)了此锁(正被B拥有,那么此时B的锁就会被删除),C就会在B正在执行的时候拿到此锁
    */
    private static String DEL_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @RequestMapping("/shopping")
    public R shopping() throws InterruptedException {
        String name = Thread.currentThread().getName();
        String lock = "lock";
        String random = UUID.randomUUID().toString();
        //此步骤同时设置key value和过期时间 保证原子性
        if (redisTemplate.opsForValue().setIfAbsent(lock, random, 60, TimeUnit.SECONDS)) {
            System.out.println(name + "=====================");
            System.out.println(name + "拿到锁了");
            System.out.println("处理业务逻辑.................");
            //代表业务逻辑需要五秒时间
            Thread.sleep(5000);
            System.out.println("处理完毕......");
            System.out.println(name + "要释放锁了");
            //先拿到此时key
            Boolean success = unLock(lock, random);
            if (success) {
                System.out.println("释放锁成功");
                return new R(Boolean.TRUE, "释放锁成功");
            }
            System.out.println("释放锁失败");
            return new R("释放锁失败");
        } else {
            System.out.println("当前系统繁忙请稍后再试");
            return new R(Boolean.FALSE, "当前系统繁忙请稍后再试");
        }
    }

    //释放锁
    private Boolean unLock(String key, String value) {
        //此步骤校验当前线程的key value 值 存在就释放锁 redis支持LUA脚本 保证原子性
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(DEL_SCRIPT, Long.class);
        //TODO :为什么返回值不用 Integer 接收而是用 Long。这里是因为 spring-boot-starter-data-redis 提供的返回类型里面不支持 Integer。
        Long execute = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
        if (1L == execute) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
}

通过 Redis 的 eval() 函数执行 Lua 脚本,其中入参 lockName 赋值给参数 KEYS[1],myRandomValue 赋值给 ARGV[1],eval() 函数将 Lua 脚本交给 Redis 服务端执行。

jedis.eval(script, Collections.singletonList(lockName), Collections.singletonList(myRandomValue));

根据 Redis 官网文档说明,通过 eval() 执行 Lua 代码时,Lua 代码将被当成一个命令去执行(可保证原子性),并且直到 eval 命令执行完成,Redis 才会执行其他命令。因此,通过 Lua 脚本结合 eval 函数,可以科学得实现解锁操作的原子性,避免误解锁。

6.锁冲突处理

客户端在处理请求时加锁没加成功怎么办。一般有 3 种策略来处理加锁失败:

1、直接抛出异常,通知用户稍后重试;

这种方式比较适合由用户直接发起的请求,用户看到错误对话框后,会先阅读对话框的内容,再点击重试,这样就可以起到人工延时的效果。如果考虑到用户体验,可以由前端的代码替代用户自己来进行延时重试控制。它本质上是对当前请求的放弃,由用户决定是否重新发起新的请求

个人理解: 大部分都直接返回用户,比如抢单(没抢到返回让用户再次点击) 分布式系统定时任务(一个抢到锁,没抢到的直接结束无需等待)

2、sleep 一会再重试;

sleep 会阻塞当前的消息处理线程,会导致队列的后续消息处理出现延迟。如果碰撞的比较频繁或者队列里消息比较多,sleep 可能并不合适。如果因为个别死锁的 key 导致加锁不成功,线程会彻底堵死,导致后续消息永远得不到及时处理。 

3、将请求转移至延时队列,过一会再试 (延时队列)

2.延时队列(不是很理解)

1.异步消息队列

Redis 的 list(列表) 数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,使用 lpop 和 rpop 来出队列。

list-(队列)

队列空了怎么办?

	客户端是通过队列的 pop 操作来获取消息,然后进行处理。处理完了再接着获取消息,再进行处理。如此循环往复,这便是作为队列消费者的客户端的生命周期。 
	可是如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据。这就是浪费生命的空轮询。空轮询不但拉高了客户端的 CPU,redis 的 QPS 也会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢查询可能会显著增多。 
	通常我们使用 sleep 来解决这个问题,让线程睡一会,睡个 1s 钟就可以了。不但客户端的 CPU 能降下来,Redis 的 QPS 也降下来了。 

队列延迟 blpop/brpop。
​ 这两个指令的前缀字符 b 代表的是 blocking,也就是阻塞读。
阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。用 blpop/brpop 替代前面的 lpop/rpop,就完美解决了上面的问题

2.空闲连接自动断开
空闲连接的问题 
	如果线程一直阻塞在哪里,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候 blpop/brpop 会抛出异常来。 
所以编写客户端消费者的时候要小心,注意捕获异常,还要重试
3.延时队列的实现
	延时队列可以通过 Redis 的 zset(有序列表) 来实现。我们将消息序列化成一个字符串作为 zset 的 value,这个消息的到期处理时间作为 score,然后用多个线程轮询 zset 获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处理。因为有多个线程,所以需要考虑并发争抢任务,确保任务不能被多次执行
	Redis 的 zrem 方法是多线程多进程争抢任务的关键,它的返回值决定了当前实例有没有抢到任务,因为 loop 方法可能会被多个线程、多个进程调用,同一个任务可能会被多个进程线程抢到,通过 zrem 来决定唯一的属主。  
	同时,我们要注意一定要对 handle_msg 进行异常捕获,避免因为个别任务处理问题导致循环异常退出。以下是 Java 版本的延时队列实现,因为要使用到 Json 序列化,所以还需要 fastjson 库的支持
@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisDelayingQueue<T> {
    static class TaskItem<T> {
        public String id;
        public T msg;
    }
    // fastjson 序列化对象中存在 generic 类型时,需要使用 TypeReference
    private Type TaskType = new TypeReference<TaskItem<T>>() {
    }.getType();

    @Autowired
    private Jedis jedis;
    private String queueKey = "q-demo";

    public RedisDelayingQueue() {}

    /*public RedisDelayingQueue(Jedis jedis, String queueKey) {
        this.jedis = jedis;
        this.queueKey = queueKey;
    }*/

    public void delay(T msg, Jedis jedis) {
        TaskItem task = new TaskItem();
        task.id = UUID.randomUUID().toString();  // 分配唯一的 uuid
        task.msg = msg;
        String s = JSON.toJSONString(task);  // fastjson 序列化
        // 塞入延时队列 ,5s 后再试
        jedis.zadd(queueKey, System.currentTimeMillis() + 5000, s);
    }

    public void loop(Jedis jedis) {
        while (!Thread.interrupted()) {   // 只取一条
            Set values = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);
            if (values.isEmpty()) {
                try {
                    Thread.sleep(500);  // 歇会继续
                } catch (InterruptedException e) {
                    break;
                }
                continue;
            }
            String s = (String) values.iterator().next();
            if (jedis.zrem(queueKey, s) > 0) {  // 抢到了
                TaskItem task = JSON.parseObject(s, TaskType);  // fastjson 反序列化
                this.handleMsg((T) task.msg);
            }
        }
    }

    public void handleMsg(T msg) {
        System.out.println(msg);
    }

    @Test
    public void main() {
//        RedisDelayingQueue queue = new RedisDelayingQueue<>(jedis, "q-demo");
        RedisDelayingQueue queue = new RedisDelayingQueue<>();
        //生产者
        Thread producer = new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    queue.delay("codehole" + i, jedis);
                }
            }
        };
        //消费者
        Thread consumer = new Thread() {
            public void run() {
                queue.loop(jedis);
            }
        };
        producer.start();
        consumer.start();
        try {
            producer.join();
            Thread.sleep(6000);
            consumer.interrupt();
            consumer.join();
        } catch (InterruptedException e) {
        }
    }
}

进一步优化

上面的算法中同一个任务可能会被多个进程取到之后再使用 zrem 进行争抢,那些没抢到的进程都是白取了一次任务,这是浪费。可以考虑使用 lua scripting 来优化一下这个逻辑,将zrangebyscore 和 zrem 一同挪到服务器端进行原子化操作,这样多个进程之间争抢任务时就不会出现这种浪费了。 

3.节衣缩食 —— 位图 (看不懂)

1.前提描述
	平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。 
	为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间
位图
位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。 
2.基本使用 (看不懂)

4.四两拨千斤 —— HyperLogLog

1.前提背景
	如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 UV 数据,然后让你来开发这个统计模块,你会如何实现? 
	如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。 但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。 
	你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。没错,这是一个非常简单的方案
	但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间,其实老板需要的数据又不需要
太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别

Redis 提供了 HyperLogLog 数据结构就是用来解决这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。 HyperLogLog 数据结构是 Redis 的高级数据结构,它非常有用,但是令人感到意外的是,使用过它的人非常少。

2.使用方法

​ HyperLogLog 提供了两个指令 pfaddpfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是。pfcount 和 scard 用法是一样的,直接获取计数值

127.0.0.1:6379> pfadd codehole user1 
(integer) 1 
127.0.0.1:6379> pfcount codehole 
(integer) 1 
127.0.0.1:6379> pfadd codehole user2 
(integer) 1 
127.0.0.1:6379> pfcount codehole 
(integer) 2 
127.0.0.1:6379> pfadd codehole user3 
(integer) 1 
127.0.0.1:6379> pfcount codehole 
(integer) 3 
3.pfmerge 适合什么场合用?

比如在网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。其中页面的 UV 访问量也需要合并,那这个时候 pfmerge 就可以派上用场了

5.层峦叠嶂 —— 布隆过滤器

1.简述
布隆过滤器,主要的效果作用就是  
查询key  不存在的时候 一定是不存在的
存在不代表一定存在
2.用法

布隆过滤器有二个基本指令:

bf.add 添加元素

bf.exists 查询元素是否存在

它的用法和 set 集合的 sadd 和 sismember 差不多。注意 bf.add 只能一次添加一个元素,如果想要一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需要用到 bf.mexists 指令。

bf.reserve (自定义布隆过滤器)

Redis 其实还提供了自定义参数的布隆过滤器,需要我们在 add 之前使用 bf.reserve指令显式创建。如果对应的 key 已经存在,bf.reserve 会报错

bf.reserve 有三个参数,

分别是 key, error_rate 和 initial_size。错误率越低,需要的空间越大。initial_size 参数表示预计放入的元素数量,当实际数量超出这个数值时,误判率会上升。
所以需要提前设置一个较大的数值避免超出导致误判率升高。如果不使用 bf.reserve,默认的 error_rate 是 0.01,默认的 initial_size 是 100。

127.0.0.1:6379> bf.add codehole user1 
(integer) 1 
127.0.0.1:6379> bf.add codehole user2 
(integer) 1 
127.0.0.1:6379> bf.add codehole user3 
(integer) 1 
127.0.0.1:6379> bf.exists codehole user1 
(integer) 1 
127.0.0.1:6379> bf.exists codehole user2 
(integer) 1 
127.0.0.1:6379> bf.exists codehole user3 
(integer) 1 
127.0.0.1:6379> bf.exists codehole user4  
(integer) 0 
127.0.0.1:6379> bf.madd codehole user4 user5 user6 
1) (integer) 1 
2) (integer) 1 
3) (integer) 1 
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7 
1) (integer) 1 
2) (integer) 1
3) (integer) 1 
4) (integer) 0 

误判率测试:

public class BloomTest {
    private String chars;
    {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 26; i++) {
            builder.append((char) ('a' + i));
        }
        chars = builder.toString();
    }

    private String randomString(int n) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < n; i++) {
            int idx = ThreadLocalRandom.current().nextInt(chars.length());
            builder.append(chars.charAt(idx));
        }
        return builder.toString();
    }

    private List<String> randomUsers(int n) {
        List<String> users = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            users.add(randomString(64));
        }
        return users;
    }

    public static void main(String[] args) {
        BloomTest bloomer = new BloomTest();
        List<String> users = bloomer.randomUsers(100000);
        List<String> usersTrain = users.subList(0, users.size() / 2);
        List<String> usersTest = users.subList(users.size() / 2, users.size());

        Client client = new Client();
        client.delete("codehole");     // 对应 bf.reserve 指令     
        client.createFilter("codehole", 50000, 0.001);
        for (String user : usersTrain) {
            client.add("codehole", user);
        }
        int falses = 0;
        for (String user : usersTest) {
            boolean ret = client.exists("codehole", user);
            if (ret) {
                falses++;
            }
        }
        System.out.printf("%d %d\n", falses, usersTest.size());
        client.close();
    }
} 
运行一下,等待约 1 分钟,输出如下: 
total users 100000
all trained
6 50000 
 
我们看到了误判率大约 0.012%,比预计的 0.1% 低很多,不过布隆的概率是有误差
的,只要不比预计误判率高太多,都是正常现象。 
3.原理

每个布隆过滤器对应到 Redis 的数据结构里面就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。

bf.add存入值的时候,首先会通过多个hash函数对key进行hash计算,会得到多个不同的位置,再把每个位置的值设置为1

bf.exits 判断是否存在

也是用hash函数计算key的位置值,看看这几个位置的值是否都为1,如果存在一个0,那就代表不存在 如果这几个值都为1,也不能说明一定存在,只是可能存在,也可能是其他的key占用了这几个位置,

如果这个位数组数值比较大,误判率就会较低,当实际值超过预定值之后,误判率就会上升 所以要根据实际情况设定适当的值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ovYwX6QJ-1624523996478)(img\布隆过滤器.jpg)]

4.注意事项

key, error_rate 和 initial_size。错误率越低,需要的空间越大。initial_size 参数表示预计放入的元素数量,当实际数量超出这个数值时,误判率会上升。
所以需要提前设置一个较大的数值避免超出导致误判率升高。如果不使用 bf.reserve,默认的 error_rate 是 0.01,默认的 initial_size 是 100。

​ 布隆过滤器的 initial_size 估计的过大,会浪费存储空间,估计的过小,就会影响准确率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多。

​ 布隆过滤器的 error_rate 越小,需要的存储空间就越大,对于不需要过于精确的场合,error_rate 设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变

5.应用场景

5.1 学习Redis时,在某种情况下,可能会出现缓存雪崩缓存穿透
缓存穿透

​ 大量请求访问时,Redis没有命中数据,导致请求绕过了Redis缓存,直接去访问数据库了。数据库难以承受大量的请求。此时便可以使用布隆过滤器来解决。请求到来时,先用布隆过滤器判断数据是否有效,布隆过滤器可以判断元素一定不存在和可能存在,对于一定不存在的数据,则可以直接丢弃请求。对可能存在的请求,再去访问Redis获取数据,Redis没有时,再去访问数据库。

5.2 邮箱的垃圾邮件过滤、黑名单等。
5.3 去重,:比如爬给定网址的时候对已经爬取过的 URL 去重。

6.断尾求生 —— 简单限流

1.前提概述

限流算法在分布式领域是一个经常被提起的话题,当系统的处理能力有限时,如何阻止计划外的请求继续对系统施压,这是一个需要重视的问题。

2.解决办法

如何使用 Redis 来实现简单限流策略?

个人理解:

当某用户多次进行一个操作时,用户id,此类操作标识,可操作次数,当前操作时间毫秒值 用redis的zadd存入,在规定时间内(假设60s)操作次数超过可操作次数,就限流

@RequestMapping("/simpleRateLimiter")
    public R SimpleRateLimiter() {
        for (int i = 0; i < 20; i++) {
            System.out.println(isActionAllowed("laoqian", "reply", 60, 5));
        }
        return null;
    }

    /**
     * @param userId
     * @param actionKey 动作键
     * @param period    时期
     * @param maxCount  最大计数
     * @return
     * @throws IOException
     */
    private boolean isActionAllowed(String userId, String actionKey, int period, int maxCount)   {
        String key = String.format("hist:%s:%s", userId, actionKey);
        long nowTs = System.currentTimeMillis();
        Pipeline pipe = jedis.pipelined();
        pipe.multi();
        pipe.zadd(key, nowTs, "value " + nowTs);

        //用来删除60秒之前的数据
        long end  = nowTs - period * 1000;
        pipe.zremrangeByScore(key, 0, end);
        Response<Long> count = pipe.zcard(key);
        //设置国企时间
        pipe.expire(key, period + 1);
        pipe.exec();
        try {
            pipe.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Long aLong = count.get();
        return aLong <= maxCount;
    }

书中这样描述,还要根据实际场景考虑

因为这几个连续的 Redis 操作都是针对同一个 key 的,使用 pipeline 可以显著提升 Redis 存取效率。但这种方案也有缺点,因为它要记录时间窗口内所有的行为记录,如果这个量很大,比如限定 60s 内操作不得超过 100w 次这样的参数,它是不适合做这样的限流的,因为会消耗大量的存储空间。 

7.一毛不拔 —— 漏斗限流

漏斗限流是最常用的限流方法之一,顾名思义,这个算法的灵感源于漏斗(funnel)的结构。

漏斗限流

漏斗的剩余空间就代表着当前行为可以持续进行的数量,漏嘴的流水速率代表着系统允许该行为的最大频率

1.漏斗逻辑算法 (大致看懂代码)
public class FunnelRateLimiter {
   static class Funnel {
        int capacity; // 漏斗容量
        float leakingRate; //漏嘴流水速
        int leftQuota; //漏斗剩余空间
        long leakingTs;//上一次漏水时间

        //漏斗
        public Funnel(int capacity, float leakingRate) {
            this.capacity = capacity;
            this.leakingRate = leakingRate;
            this.leftQuota = capacity;
            this.leakingTs = System.currentTimeMillis();
        }

        //腾出空间
        void makeSpace() {
            long nowTs = System.currentTimeMillis();
            //距离上一次漏水过去了多久
            long deltaTs = nowTs - leakingTs;
            //又可以腾出不少空间了  可以增加的空间
            //时间间隔 * 漏嘴流水速率 quota/s = 增加的空间
            int deltaQuota = (int) (deltaTs * leakingRate);
            // 间隔时间太长,整数数字过大溢出
            if (deltaQuota < 0) {
                this.leftQuota = capacity;
                this.leakingTs = nowTs;
                return;
            }
            //增加的空间太少,那就等下次吧
            // 增加的空间太小,最小单位是 1
            if (deltaQuota < 1) {
                return;
            }
            //增加剩余空间
            this.leftQuota += deltaQuota;
            // 记录漏水时间
            this.leakingTs = nowTs;
            //剩余空间不得高于容量
            //如果剩余的空间大于总容量 就=总容量
            if (this.leftQuota > this.capacity) {
                this.leftQuota = this.capacity;
            }
        }

        boolean watering(int quota) {
            //腾出空间
            makeSpace();
            //判断剩余空间是否足够
            if (this.leftQuota >= quota) {
                this.leftQuota -= quota;
                return true;
            }
            return false;
        }
    }

    //所有的漏斗
    private static Map<String, Funnel> funnels = new HashMap<>();

    /**
     *
     * @param userId
     * @param actionKey
     * @param capacity 漏斗容量
     * @param leakingRate  漏嘴流水速率 quota/s
     * @return
     */
    private static boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate) {
        String key = String.format("%s:%s", userId, actionKey);
        Funnel funnel = funnels.get(key);
        if (funnel == null) {
            funnel = new Funnel(capacity, leakingRate);
            funnels.put(key, funnel);
        }
        return funnel.watering(1); // 需要 1 个 quota
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            boolean actionAllowed = isActionAllowed("jlz", "reply", 15, 0.5f);
            System.out.println(actionAllowed);
        }
    }
}

Funnel 对象的 make_space 方法是漏斗算法的核心,其在每次灌水前都会被调用以触发漏水,给漏斗腾出空间来。能腾出多少空间取决于过去了多久以及流水的速率。Funnel 对象占据的空间大小不再和行为的频率成正比,它的空间占用是一个常量

2.缺点
	无法保证整个过程的原子性。从 hash 结构中取值,然后在内存里运算,再回填到 hash 结构,这三个过程无法原子化,意味着需要进行适当的加锁控制。而
一旦加锁,就意味着会有加锁失败,加锁失败就需要选择重试或者放弃。 
	如果重试的话,就会导致性能下降。如果放弃的话,就会影响用户体验。同时,代码的复杂度也跟着升高很多。这真是个艰难的选择,如何解决这个问题呢?Redis-Cell 救星来了
3.Redis-Cell

Redis 4.0 提供了一个限流 Redis 模块,它叫 redis-cell。该模块也使用了漏斗算法,并提供了原子的限流指令。有了这个模块,限流问题就非常简单了

该模块只有 1 条指令

cl.throttle

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tvprkfj8-1624523996479)(img\redis-cell.jpg)]

	上面这个指令的意思是允许「用户老钱回复行为」的频率为每 60s 最多 30 次(漏水速率),漏斗的初始容量为 15,也就是说一开始可以连续回复 15 个帖子,然后才开始受漏水速率的影响。我们看到这个指令中漏水速率变成了 2 个参数,替代了之前的单个浮点数。用两个参数相除的结果来表达漏水速率相对单个浮点数要更加直观一些。 
redis-cell解释
  • 1) 是否成功,0:成功,1:拒绝
  • 2) 令牌桶的容量,大小为初始值+1
  • 3) 当前令牌桶中可用的令牌
  • 4) 若请求被拒绝,这个值表示多久后才令牌桶中会重新添加令牌,单位:秒,可以作为重试时间
  • 5) 表示多久后令牌桶中的令牌会存满

由于redis-Cell是基于Rust语言写的插件,因此在安装插件前要先安装rust, 具体可参看官方README github

JAVA使用:

创建接口

package study.redis.test;

import io.lettuce.core.dynamic.Commands;
import io.lettuce.core.dynamic.annotation.Command;

import java.util.List;

/**
 * @author jlz
 * 漏斗使用
 */
public interface RedisCommandInterface extends Commands {
    /**
     * 限流,如CL.THROTTLE test 10 5 60 1
     * <p>
     * test是key名字,初始10个令牌,每60秒允许使用5个令牌,本次获取一个令牌
     * <p>
     * 127.0.0.1:6379> CL.THROTTLE test 10 5 60 1
     * <p>
     * 1) (integer) 0 0成功,1拒绝
     * <p>
     * 2) (integer) 11 令牌桶的容量
     * <p>
     * 3) (integer) 10 当前令牌桶中可用的令牌
     * <p>
     * 4) (integer) -1 若请求被拒绝,这个值表示多久后才令牌桶中会重新添加令牌,单位:秒,可以作为重试时间
     * <p>
     * 5) (integer) 12 表示多久后令牌桶中的令牌会存满
     *
     * @param key
     * @param initCapactiy
     * @param operationCount
     * @param secondCount
     * @param getCount
     * @return
     */
    @Command("CL.THROTTLE ?0 ?1 ?2 ?3 ?4")
    List<Object> throttle(String key, long initCapactiy, long operationCount, long secondCount, long getCount);
}

使用

package study.util;

import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.dynamic.RedisCommandFactory;
import study.redis.test.RedisCommandInterface;

import java.util.List;

/**
 * @author jlz
 * 漏斗
 */
public class RedisUtil {
    private static RedisClient redisClient = null;
    private static StatefulRedisConnection<String, String> conn = null;
    private static RedisCommandInterface commands;
    private static long initCapactiy = 20;
    private static long operationCount = 20;
    private static long secondCount = 1;
    private static long getCount = 1;

    static {
        // 创建连接(会自动重连)
        redisClient = RedisClient.create("redis://xxxx@127.0.0.1:6379/0");
        conn = redisClient.connect();
        // 获取Command
        RedisCommandFactory factory = new RedisCommandFactory(conn);
        commands = factory.getCommands(RedisCommandInterface.class);
    }

    /**
     * 漏斗使用
     * @param name
     * @return
     */
    public static boolean isAllowQuery(String name) {
        //最多等两次
        for (int i = 0; i < 3; i++) {
            List<Object> resultList = commands.throttle(name, initCapactiy, operationCount, secondCount, getCount);

            if (resultList.get(0).toString().equals("0")) {
                return true;
            }
            //睡半秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println("fail," + name);
        return false;
        // Luna脚本不认这个命令
        // RedisCommands<String, String> cmds = conn.sync();
        // List<String> resultList = cmds.eval(" return redis.call('CL.THROTTLE test 10
        // 5 60 1') ",ScriptOutputType.MULTI);
        //
    }
}
4.应用场景
5.总结
  • 从限制用户行为频率场景出发,引入redis-cell分布式限流解决方案,阐述了其算法原理及步骤,并给出实例说明;
  • 频率限制的实现有多种方式,例如NginxHaproxy都有限制模块、通过Redis来实现也是常见的方式之一;
  • 除了引入redis-cell分布式限流模块, 也可以将上述令牌通的实现思路通过Lua脚本实现,然后嵌入到redis中执行, 实际上在redis还不支持redis-cell模块时, 实际使用场景中大多采用redis+lua方式来实现限流策略;
  • redis-cell限流应用于微服务接口访问频次上也非常方便;
6.问题描述
某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。

查看漏斗算法令牌桶算法

7.相关算法
1.漏斗算法

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

个人理解: 此算法的作用是限定一定的速率来处理请求,因为出口就那么大,虽然能够达到限速的效果,但是突发的大量请求到达,不能快速的处理请求

如下图

漏斗算法

缺点 :

	对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图2所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
2.令牌桶算法

原理 :令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

个人理解 :

  • 一边通过定时任务不断向令牌桶放入令牌
  • 请求过来是先获取令牌 有令牌就处理业务,没令牌就返回
  • 用的是redis的list数据结构
令牌桶算法

令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。

也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。

依靠上述的思想,我们可以结合Redis的List数据结构很轻易的做到这样的代码,只是简单实现

定时任务存令牌:
	// 10S的速率往令牌桶中添加UUID,只为保证唯一性
    @Scheduled(fixedDelay = 10_000,initialDelay = 0)
    public void setIntervalTimeTask(){
        redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
    }
// 输出令牌
public Response limitFlow2(Long id){
        Object result = redisTemplate.opsForList().leftPop("limit_list");
        if(result == null){
            return Response.ok("当前令牌桶中无令牌");
        }
        return Response.ok(articleDescription2);
    }
3.总结:

并不能说明令牌桶一定比漏洞好,她们使用场景不一样。

令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。

而漏桶算法,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。

总结起来

  • 如果要让自己的系统不被打垮,用令牌桶。
  • 如果保证被别人的系统不被打垮,用漏桶算法。

8.近水楼台 —— GeoHash (附近的人)[暂时了解]

1.GeoHash算法描述
	GeoHash 算法会继续对这个整数做一次 base32 编码 (0-9,a-z 去掉 a,i,l,o 四个字母) 变成一个字符串。在 Redis 里面,经纬度使用 52 位的整数进行编码,放进了 zset 里面,zset 的 value 是元素的 key,score 是 GeoHash 的 52 位整数值。zset 的 score 虽然是浮点数,但是对于 52 位的整数值,它可以无损存储。 
	在使用 Redis 进行 Geo 查询时,我们要时刻想到它的内部结构实际上只是一个 zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 (实际情况要复杂一些,不过这样理解足够了),通过将 score 还原成坐标值就可以得到元素的原始坐标。 
2.Redis的Geo命令基本使用

Redis 提供的 Geo 指令只有 6 个,读者们瞬间就可以掌握。使用时,它只是一个普通的 zset 结构。

1.增加

geoadd 指令携带集合名称以及多个经纬度名称三元组,注意这里可以加入多个三元组

127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin 
(integer) 1 
127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader 
(integer) 1 
127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan 
(integer) 1 
127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi
(integer) 2 

Redis 没有提供 geo 删除指令

2.计算距离

geodist 指令可以用来计算两个元素之间的距离,携带集合名称、2 个名称和距离单位

127.0.0.1:6379> geodist company juejin ireader km 
"10.5501" 
127.0.0.1:6379> geodist company juejin meituan km 
"1.3878" 
127.0.0.1:6379> geodist company juejin jd km 
"24.2739" 
127.0.0.1:6379> geodist company juejin xiaomi km 
"12.9606" 
127.0.0.1:6379> geodist company juejin juejin km 
"0.0000" 

距离单位可以是 m、km、ml、ft,分别代表米、千米、英里和尺。

3.获取元素位置

geopos 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个

127.0.0.1:6379> geopos company juejin 
1) 1) "116.48104995489120483"    
	2) "39.99679348858259686" 
127.0.0.1:6379> geopos company ireader 
1) 1) "116.5142020583152771"    
	2) "39.90540918662494363" 
127.0.0.1:6379> geopos company juejin ireader 
1) 1) "116.48104995489120483"    
	2) "39.99679348858259686" 
2) 1) "116.5142020583152771"    
	2) "39.90540918662494363" 

获取的经纬度坐标和 geoadd 进去的坐标有轻微的误差,原因是 geohash 对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。对于「附近的人」这种功能来说,这点误差根本不是事。

4.获取元素的 hash 值

geohash 可以获取元素的经纬度编码字符串

127.0.0.1:6379> geohash company ireader 
1) "wx4g52e1ce0" 
127.0.0.1:6379> geohash company juejin 
1) "wx4gd94yjn0" 
5.georadiusbymember (关键)–>附近的公司

georadiusbymember 指令是最为关键的指令,它可以用来查询指定元素附近的其它元素,它的参数非常复杂

// 范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身 
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc 
1) "ireader"
2) "juejin" 
3) "meituan" 

// 范围 20 公里以内最多 3 个元素按距离倒排 
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc 
1) "jd" 
2) "meituan" 
3) "juejin" 

// 三个可选参数 withcoord withdist withhash 用来携带附加参数 
// withdist 很有用,它可以用来显示距离 
127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc 
1) 1) "ireader"    
	2) "0.0000"    
	3) (integer) 4069886008361398    
	4)  1) "116.5142020583152771"       
		2) "39.90540918662494363" 
2) 1) "juejin"    
	2) "10.5501"    
	3) (integer) 4069887154388167    
	4)	1) "116.48104995489120483"       
		2) "39.99679348858259686" 
3) 1) "meituan"    
	2) "11.5748"    
	3) (integer) 4069887179083478    
	4)  1) "116.48903220891952515"      
		2) "40.00766997707732031" 

除了 georadiusbymember 指令根据元素查询附近的元素,Redis 还提供了根据坐标值来查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近的餐馆」等。它的参数和 georadiusbymember 基本一致,除了将目标元素改成经纬度坐标值。

georadius

127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc 
1) 1) "ireader"    
	2) "0.0000" 
2) 1) "juejin"    
	2) "10.5501" 
3) 1) "meituan"    
	2) "11.5748" 
6.小结 & 注意事项
	在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。 
	所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。 如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。 

9.大海捞针 —— Scan

1.前提

有时候需要从 Redis 实例成千上万的 key 中找出特定前缀的 key 列表来手动处理数据,可能是修改它的值,也可能是删除 key

keys : Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key。

127.0.0.1:6379> keys * 
1) "codehole1" 
2) "code3hole" 
3) "codehole3" 
127.0.0.1:6379> keys codehole* 
1) "codehole1" 
2) "codehole3" 
3) "codehole2" 
127.0.0.1:6379> keys code*hole 
1) "code3hole" 
2) "code2hole" 
3) "code1hole" 
2.keys 缺点:

1、没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个 key 满足条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了。
2、keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。

3.scan 解决
4.scan 相比 keys 具备有以下特点:
  1. 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程;
  2. 提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的结果可多可少;
  3. 同 keys 一样,它也提供模式匹配功能;
  4. 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
  5. 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
  6. 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  7. 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;
5.scan 基础使用

SCAN cursor [MATCH pattern] [COUNT count]

scan 参数提供了三个参数

第一个是 cursor 整数值

第二个是 key 的正则模式

第三个是遍历的 limit hint。指的是游标

第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

测试 : 往 Redis 里插入 10000 条数据来进行测试

127.0.0.1:6379> scan 0 match key99* count 1000
1) "13976" 
2)  
    1) "key9911"     
    2) "key9974"     
    3) "key9994"     
    4) "key9910"     
    5) "key9907"   
    ......省略
127.0.0.1:6379> scan 13976 match key99* count 1000 
1) "1996" 
2)  
    1) "key9982"     
    2) "key9997"     
    3) "key9963"     
    4) "key996"     
    5) "key9912"    
        ......省略
127.0.0.1:6379> scan 1996 match key99* count 1000 
1) "0" 
2) 
    1) "key9939"    
    2) "key9941"    
    3) "key9967"    
    4) "key9938"    
    5) "key9906"
        ......省略

​ 从上面的过程可以看到虽然提供的 limit 是 10,但是返回的结果只有 5个左右。因为这个 limit 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于)。如果将 limit 设置为 10,你会发现返回结果是空的,但是游标值不为零,意味着遍历还没结束

127.0.0.1:6379> scan 0 match key99* count 10 
1) "3072" 
2) (empty list or set) 
6.字典结构 (与hashMap类似)

​ 在 Redis 中所有的 key 都存储在一个很大的字典中,这个字典的结构和 Java 中的 HashMap 一样,是一维数组 + 二维链表结构,第一维数组的大小总是 2^n(n>=0),扩容一次数组大小空间加倍,也就是 n++。

scan字典结构

​ scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将 limit 数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端

个人理解: 游标指得就是数组中的位置,也就是对用的槽,每次scan时,找到此槽下的链表进行匹配过滤,返回结果

7.scan 遍历顺序

​ scan 的遍历顺序非常特别。它不是从第一维数组的第 0 位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
普通加法和高位进位加法的区别 :
​ 高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历所有的槽位并且没有重复

8.scan的扩容(也与hashMap类似 但有不同)

没看懂 暂时不写

9.渐进式 rehash

​ Java 的 HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。如果 HashMap 中元素特别多,线程就会出现卡顿现象。Redis 为了解决这个问题,它采用渐进式 rehash。
​ 它会同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作中渐渐地将旧数组中挂接的元素迁移到新数组上。这意味着要操作处于 rehash 中的字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面去寻找。
​ scan 也需要考虑这个问题,对与 rehash 中的字典,它需要同时扫描新旧槽位,然后将
结果融合后返回给客户端

10.更多的scan指令

​ scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进行遍历。比如 zscan 遍历 zset 集合元素,hscan 遍历 hash 字典的元素、sscan 遍历 set 集合的元素。
​ 它们的原理同 scan 都会类似的,因为 hash 底层就是字典,set 也是一个特殊的 hash(所有的 value 指向同一个元素),zset 内部也使用了字典来存储所有的元素内容,所以这里不再赘述。

11.存在数据过大的key

​ 有时候会因为业务人员使用不当,在 Redis 实例中会形成很大的对象,比如一个很大的 hash,一个很大的 zset 这都是经常出现的。这样的对象对 Redis 的集群数据迁移带来了很大的问题,因为在集群环境下,如果某个 key 太大,会数据导致迁移卡顿。另外在内存分配上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。

在平时的业务开发中,要尽量避免大 key 的产生。
如果你观察到 Redis 的内存大起大落,这极有可能是因为大 key 导致的,这时候你就需要定位出具体是那个 key,进一步定位出具体的业务来源,然后再改进相关业务代码设计。

定位大 key:

​ 为了避免对线上 Redis 带来卡顿,这就要用到 scan 指令,对于扫描出来的每一个 key,使用 type 指令获得 key 的类型,然后使用相应数据结构的 size 或者 len 方法来得到它的大小,对于每一种类型,保留大小的前 N 名作为扫描结果展示出来。
​ 上面这样的过程需要编写脚本,比较繁琐,不过 Redis 官方已经在 redis-cli 指令中提供了这样的扫描功能,我们可以直接拿来即用。

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys 

​ 如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参数。

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1 

上面这个指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的时间会变长。

4.原理篇:

1.鞭辟入里 —— 线程 IO 模型 (看不懂)

1.前景描述

Redis 是个单线程程序!这点必须铭记

也许你会怀疑高并发的 Redis 中间件怎么可能是单线程。很抱歉,它就是单线程,你的怀疑暴露了你基础知识的不足。莫要瞧不起单线程,除了 Redis 之外,Node.js 也是单线程,Nginx 也是单线程,但是它们都是服务器高性能的典范。 

Redis 单线程为什么还能这么快?

因为它所有的数据都在内存中,所有的运算都是内存级别的运算。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些时间复杂度为 O(n) 级别的指令,一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。 

Redis 单线程如何处理那么多的并发客户端连接? (个人感觉是重点)

这个问题,有很多中高级程序员都无法回答,因为他们没听过多路复用这个词汇,不知道 select 系列的事件轮询 API,没用过非阻塞 IO

2.非阻塞 IO

3.事件轮询 (多路复用)

个人理解 : 当正在读/写的时候,有新的任务进来,就放下目前的,

4.指令队列

Redis 会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理,先到先服务。

个人理解: 就是说每个客户端都有一个暂存命令的队列,服务端按照队列顺序处理

5.响应队列
	Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从 write_fds 里面移出来。等到队列有数据了,再将描述符放进去。避免 select 系统调用立即返回写事件,结果发现没什么数据可以写。出这种情况的线程会飙高 CPU。 

6.定时任务

2. 未雨绸缪 —— 持久化

1.快照
1.概述

​ 快照是一次全量备份。快照是内存数据的二进制序列化形式,在存储上非常紧

2.RDB快照
1.前提描述
	Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写
	Redis 还需要进行内存快照,内存快照要求 Redis 必须进行文件 IO 操作,可文件 IO 操作是不能使用多路复用 API
	这意味着单线程同时在服务线上的请求还要进行文件 IO 操作,文件 IO 操作会严重拖垮服务器请求的性能。还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢

​ Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制 很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标。

2.快照原理
1.fork(多进程)

​ Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进 程来处理,父进程继续处理客户端请求

3.AOF
1.概述

​ AOF 日志是连续的增量备份,AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身

2.原理

​ Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文 本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已 经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态

4.Redis 4.0 混合持久化

​ Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可 以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

3.同舟共济 —— 事务

1.

4.有备无患 —— 主从同步

1.前提
	有了主从,当 master 挂掉的时候,运维让从库过来接管,服务就可以继续,否则 master 需要经过数据恢复和重启的过程,这就可能会拖很长的时间,影响线上业务的持续服务。
2.CAP 原理
  • C - Consistent ,一致性
  • A - Availability ,可用性
  • P - Partition tolerance ,分区容忍性

CAP 原理就是——网络分区发生时,一致性和可用性两难全。

3.最终一致

​ Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。 当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节 点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。

​ Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。 当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节 点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。

6.定时任务

2. 未雨绸缪 —— 持久化

1.快照
1.概述

​ 快照是一次全量备份。快照是内存数据的二进制序列化形式,在存储上非常紧

2.RDB快照
1.前提描述
	Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写
	Redis 还需要进行内存快照,内存快照要求 Redis 必须进行文件 IO 操作,可文件 IO 操作是不能使用多路复用 API
	这意味着单线程同时在服务线上的请求还要进行文件 IO 操作,文件 IO 操作会严重拖垮服务器请求的性能。还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢

​ Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制 很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标。

2.快照原理
1.fork(多进程)

​ Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进 程来处理,父进程继续处理客户端请求

3.AOF
1.概述

​ AOF 日志是连续的增量备份,AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身

2.原理

​ Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文 本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已 经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态

4.Redis 4.0 混合持久化

​ Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可 以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

3.同舟共济 —— 事务

1.

4.有备无患 —— 主从同步

1.前提
	有了主从,当 master 挂掉的时候,运维让从库过来接管,服务就可以继续,否则 master 需要经过数据恢复和重启的过程,这就可能会拖很长的时间,影响线上业务的持续服务。
2.CAP 原理
  • C - Consistent ,一致性
  • A - Availability ,可用性
  • P - Partition tolerance ,分区容忍性

CAP 原理就是——网络分区发生时,一致性和可用性两难全。

3.最终一致

​ Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。 当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节 点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。

​ Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。 当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节 点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值