Redis 深度历险 学习笔记


第一篇 基础和应用篇

1.1 Redis可以做什么

缓存最近热帖内容(hash)。
分布式锁(hash、string)。
记录帖子的点赞数,评论数和点击数(hash)。
记录用户帖子ID列表(zset)
记录帖子点赞用户id,评论id列表,用于显示和去重(zset)
记录帖子相关文章ID,根据内容推荐相关帖子(list)
帖子id自增,可以使用redis来分配帖子ID(计数器)
收藏集和帖子之间关系(zset)
记录热帖列表,总热榜和分类热榜(zset)
缓存用户历史行为,过滤恶意行为(zset、hash)
消息队列(zset、list、set),延迟队列(zset)
限流(hash)

1.2 Redis基础数据结构

5种基础数据结构:string(字符串),list(列表),hash(散列),set(集合),zset(有序集合)
string(字符串)
动态字符串,类似于java的ArrayList
用途:缓存用户信息。
扩容规则:当长度小于1MB时,每次扩容为加倍当前容量,当长度超过1MB时,只会扩容1MB。最大容量为512MB。

>set name codehole
>get name codehole
>del name
>mset name1 boy name2 girl name3 unknown   #合并设置
>mget name1 name2 name3  #合并取
>expire name 5    #设置过期时间
>setex name 5 codehole
>set age 10 
>incr age    # 自增
>incrby age 5     #值超出signed long最大和最小会报错

list(列表)
类似于java的LinkedList,链表,插入删除快,索引定位慢。
用途:异步队列
元素较少的时候使用ziplist,数据量较多时转为quicklist。quicklist 是将ziplist用指针链接起来形成的链表结构。既满足快速插入删除性能,又没用太大空间冗余。(后续有详细介绍)

>rpush books python java golang    # 同lpush    rpush+rpop 栈   rpush+lpop 队列
>lpop books  # 同 rpop
>llen books  
>lindex books 1  # 获取index元素,遍历,性能差
>lrange books 0 -1  # 获取区间内所有元素  0 -1  表示所有元素 
>ltrim books 1 -1  # 保留区间内元素  0 -1 表示所有元素
>ltrim books 1 0  # 区间为负  清空

hash(散列)
类似于java的HashMap,扩充时采取的是渐进式rehash策略,同时保留新旧hash结构,逐步的将旧数据迁移到新hash。
用途:存储用户信息,可只取部分信息。

>hset books java "Thinking in java"
>hgetall books
>hlen books
>hmset books java "effective java" python "learning python"
>hset user-plane age 10   # 单个子key也可以进行计数
>hincrby user-plane age 1

set(集合)
相当于java中的HashSet,内部无序,且唯一。

>sadd books python
>smembers books   # 列出所有元素,无序
>sismember books java  # 判断是value否有存在,等同于contains()
>scard books   # 统计长度
>spop books  # 弹出一个元素

zset(有序集合)
类似于java中SortedSet和HashMap结合,保证唯一,且根据score权重排序。通过跳跃列表实现(后续有详细介绍)。
用途:储存value用户id和score成绩,可根据成绩排序;存储粉丝列表,value粉丝id,score关注时间,可按关注时间排序。

>zadd books 9.0 "Thinking in java"
>zrange books 0 -1    # 按正序排序列出  参数为区间范围
>zrevrange books 0 -1   # 按倒序排序列出
>zcard books   # 统计个数
>zscore books "Thinking in java"   # 查询score 
>zrank books "Thinking in java"   # 查询排名
>zrangebyscore books 0 0.9   # 根据分值区间遍历
>zrangebyscore books -inf 0.9 wihtscores   # 根据分值区间遍历,同时返回分值
>zrem books "Thinking in java"   # 删除元素

通用规则:
1.create if not exists
2.drop if no elements
过期时间:
过期时间以整个对象为单位,而不是某个子key
已设置过期时间,调用set,过期时间失效

>set codehole yoyo
>expire codehole 600  # 设置过期时间
>ttl codehole   # 查询过期时间
>set codehole yyy

1.3 分布式锁

redis分布式锁本质是占坑,其他进行要占坑时,放弃或稍后再试,通过setnx占坑,del释放,为了防止del未被释放,可以给锁加过期时间。(后续有详细介绍)

>setnx lock:codehole true
>expire lock:codehole 5   # 设置过期时间
>del lock:codehole
>set lock:codehole true ex 5 nx   # 同时设置锁和过期时间,原子操作

超时问题:超时操作还未完成,不要做较长任务。好像没用说什么特别好的解决办法。
可重入性:对客户端set方法进行包装,使用线程的ThreadLocal存储当前持有锁计数。

1.4 延时队列

异步消息队列:用list作为异步消息队列使用。
队列为空:通过sleep让线程睡一下;也可以通过阻塞读,lpop/rpop。链接闲置过久会自动断开,阻塞读会抛异常。
锁冲突处理:直接抛出异常,让用户重试;sleep再重试;请求转至延时队列,避开冲突。
延时队列:通过zset实现,消息序列化为zset的value,到期处理时间作为score。通过多线程轮询zset获取到期任务进行处理。

1.5 位图

用途:记录用户365天签到记录
就是普通的string,byte数组,只是可以把值看成位数组,位数组会自动扩充0。零存整取,零存零取,整存零取。

>setbit s 1 1
>setbit s 15 1
>get s   # 整取
>getbit s 15 # 零取
>set w hello  # 整存
>bitcount w # 统计1的位数
>bitcount w 0 0 # 统计第一个字符1的位数
>bitcount w 0 1 # 统计前两个字符1的位数
>bitpos w 0 # 第一个0位
>bitpos w 1 1 1 # 从第二个字符算起 第一个1位

TODO bitfield

1.6 HyperLogLog

用途:提供不精确的去重统计,如页面每天用户访问数量。
需要占用12KB存储空间,不要用于统计单个用户数据,标准误差0.81%。
pfmerge可将多个pf计数器累加形成新pf值。
实现原理:没看懂 TODO

>pfadd codehole user1
>pfcount codehole

1.7 布隆过滤器

判断某个值是否存在时,如果不存在则一定不存在,存在时,可能不存在。
用途:推荐新闻,看过不推荐;爬虫系统URL去重;垃圾邮件过滤。

>bf add codehole user1
>bf exists codehole user2
>bf.madd codehole user3 user4
>bf.mexists codehole user4 user5

原理:对应到redis中一个大型位数组和几个无偏hash函数,无偏hash函数可以将hash值算的比较均匀,元素映射的位置比较随机。添加元素时,将元素通过多个hash函数对key进行hash,将多个对应位置的值设为1。判断jey是否存在时,同样通过hash函数进行hash,取出对应位置的值,如果有一个为0表示这个key不存在,如果都是1,表示可能存在这个key。
无法重新扩容,如需重新扩容必须额外记录存储器中的所有元素。

1.8 简单限流

用途:限制用户规定时间内操作允许次数。

public class SimpleRateLimiter {
	private Jedis jedis;
	public SimpleRateLimiter(Jedis jedis) {
        this.jedis = jedis;
	}
    /**
    * 思想:zadd(用户id+行为)作为key的多个值,使用(zremrangebysecore key 0 当前时间-时间窗口长度 )来除去时间窗口外的记录
    * zcard统计key中存在的value的个数即是用户请求的次数
    */    
	public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
        String key = String.format("hist:%s:%s", userId, actionKey);   // 生成key
        long nowTs = System.currentTimeMillis();
        Pipeline pipe = jedis.pipelined();
        pipe.multi();
        pipe.zadd(key, nowTs, "" + nowTs);  // 添加操作
        pipe.zremrangeByScore(key, 0, nowTs - period * 1000);   //  删除限定时间之外的操作
        Response<Long> count = pipe.zcard(key);  // 统计当前用的的操作次数
        pipe.expire(key, period + 1);  // 给当前操作设置过期时间
        pipe.exec();
        pipe.close();
        return count.get() <= maxCount;   //  如果当前有效期内操作数大于规定最大操作数,返回false
	}
    public static void main(String[] args) {
        Jedis jedis = new Jedis();
        SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
        for(int i=0;i<20;i++) {
        System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
        }
    }
}

1.9 漏斗限流

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;
           int deltaQuota = (int) (deltaTs * leakingRate);
           if (deltaQuota < 0) {    // 空了
               //间隔时间太长,整数数字过大溢出
               this.leftQuota = capacity;
               this.leakingTs = nowTs;
               return;
           }
           if (deltaQuota < 1) {   // 满了
               //腾出空间太小,最小单位是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 Map<String, Funnel> funnels = new HashMap<>();
	public 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);
        }
        //需要1个quota
        return funnel.watering(1);
	}
}

redis 4.0 提供了限流模块redis-cell

>cl.throttle plane:reply 15 30 60 1   #  key:plane:reply 容量:15 速率:30 operations / 60 seconds 可选参数:1 (默认1)

1.10 GeoHash

GeoHash算法,将经纬度映射到一维数组,越靠近,映射后的点距离越近。
用途:附近的人。
内部实际是zset(skiplist),通过score排序可的附近坐标元素。

>geoadd company 116.48 39.99 juejin
>geodist company juejin jindong km  # 获取两个地方的距离
>geopos company juejin   # 获取位置
>geohash company juejin   # 获取节点hash值,可自行转为经纬度
>georadiusbymember company juejin 20 km count 3 asc   # 查询指定元素附近的其他元素(asc正序,desc倒序)
>georadiusbymember company juejin 20 km count 3 withscoord withdist withhash asc  # withdis 显示距离
>georadius compant 116.48 39.90 20 km withdist count 3 asc # 获取某经纬度附近的元素

1.11 scan

keys : 用于列出所有满足特定正则的key。
缺点:没用offset、limit,一次性突出所有满足条件的key;是遍历算法,单线程redis,数据量过大会导致redis服务卡顿。
解决办法:scan
通过游标分布进行,不会阻塞线程;提供limit参数;提供模糊匹配;返回结果可能重复;按槽数采用高进位加法遍历key存在的大字典。

127.0.0.1:6379> keys name*
1) "name"
2) "name2"
3) "name3"
4) "name1"
127.0.0.1:6379> scan 0 match name* count 2   
1) "4"  # 没有遍历完,
2) (empty list or set)   #  不代表没有匹配的元素

第二篇 原理篇

2.1 线程IO模型

Redis是个单线程程序。
所有数据都在内存中,所有运算都是内存级别的运输,所以快。对于时间复杂度为O(n)的指令一定要小心谨慎,避免使Redis造成卡顿。
Node.js, Nginx也是单线程,也都是高性能。
Redis是单线程,那如何处理高并发客户端连接?
通过”多路复用IO“。
阻塞IO:需要进行读写操作时线程阻塞,如果要读n个字,没有读到数据,那么线程会一直阻塞,直到新的数据返回或者连接关闭,读方法才会返回,线程才能继续处理。
非阻塞IO:读写方法不阻塞,而是取决于缓存区内有多少数据,或者有多少空间能够写数据。
事件轮询(多路复用IO)
读数据没有读完,当数据到来时,线程如何得知?当写缓存区满了,剩下的数据何时才能继续写?
操作系统提供给用户事件轮询API,通过轮询去获取可处理事件,无事件操作时,最多等待timeout的时间,线程处于阻塞状态,一旦期间有事件过来,立刻返回。时间过后还没事件到来,立刻返回。拿到事件,处理完事件后继续轮询。
Redis为每个客户端套接字都关联一个指令队列。
Redis同样为每个客户端套接字关联一个响应队列。
Redis定时任务记录在”最小堆“中,每个循环周期,Redis将最小堆中到达时间点的任务进行处理,处理完成后记录下最快需要执行任务的事件,也就是timeout参数,然后休眠timeout时间。

2.2 通信协议

Redis作者认为数据库性能瓶颈并非网络流量,而是数据库自身逻辑处理,因此Redis使用了浪费流量的文本协议,依然不影响Redis高访问性能。
REST:redis序列化协议,REST将传输数据结构分为5种最小单元类型,单元结束统一加上回车换行符\r\n。

单行字符串以”+“符号开头
多行字符串以”$"符号开头,后面跟字符串长度
整数值以“:"符号开头,后跟整数的字符串形式
错误信息以“-”符号开头
数组以“*”号开头,后跟数组的长度
>scan 0
1) "0"
2) 1) "info"
   2) "books"
   3) "author"

序列化

*2
$1
0
*3
$4
info
$5
books
$6
author

2.3 持久化

Redis特点之一,保障数据不会因为故障而丢失。
有两种机制,快照和AOF日志。
快照是一次全量备份,AOF日志是连续的增量备份。快照是内存数据的二进制序列化形式,存储紧凑。AOF日志是内存数据修改的指令记录文本,长时间运行AOF日志会庞大无比,数据库加载AOF日志重放时时间会非常漫长,定期需要对AOF进行瘦身。
快照
利用操作系统的COW(Copy On Write)机制实现快照持久化。
Redis持久化时调用glibc的fork函数产生子进程,通过子进程处理持久化操作,父进程处理客户端请求。父子进程在最开始的时候和连体婴儿一样共享资源。
AOF日志
收到客户端指令,执行指令然后将日志存盘。重放AOF非常耗时,通过bgrewriteaof指令对AOF日志瘦身。
当程序对AOF日志进行写操作时,实际上是将内容写入一个内存缓存中,然后异步将数据刷回磁盘。
通过fsync(int fd)函数强制将缓存刷到磁盘,fsync操作很慢,一般生产环境每隔1s左右执行一次fsync。
快照通过开启子线程方式进行,遍历整个内存,大块写磁盘加重系统负担。
AOF的fsync是一个耗时的IO操作,会降低性能,也会增加系统IO负担。
因此,持久化操作主要在从节点进行。
Redis4.0提供混合持久化
将rdb文件内容和AOF日志文件存在一起,此时的AOF文件是子持久化开始到持久化结束这段时间发生的增量AOF日志。

2.4 管道

通过改变指令列表的读写顺序大幅节省IO时间,打包发送读数据,打包发送写数据,减少网络次数以此节省IO时间。
write操作只负责将数据写到本地缓存,如果缓存满了,此时需要等待空闲缓存,这个等待时间是写操作的真正耗时。
read操作只负责从本地缓存中取数据,如果缓存为空,此时需要等待新数据到来,这个等待时间是读操作真正的耗时。

2.5 事务

redis事务提供multi、exec、discard指令。multi指事务开始,exec指事务结束,discard指multi到discard之间操作都丢弃。在exec之前,所有操作都缓存在服务器的事务队列中,当接受exec时才开始执行事务队列,执行完毕后一次性返回所有运行结果。即使当中有操作失败,后续指令也会继续执行,也就是redis不保证原子性。
通过watch乐观锁解决并发问题。
watch在事务执行前盯住一个或多个变量,当事务exec时,检查watch的变量是否被修改,如果修改了,exec则返回null。watch必须在multi之前执行。

>watch books
ok
>incr books  # 修改watch的变量
(integer) 1
>multi
ok
>incr books
QUEUED
>exec
(nil)   # 执行失败

2.6 PubSub

通过pubsub提供消息多播。
必须先启动消费者,再执行生产者。
消费者可以通过getMeassage()轮询获得消息,获取不到则休眠,还可以通过listen阻塞监听消息进行处理。
缺点:生产者发布消息,如果没用一个消费者,消息会被直接丢弃;如果有消费者,部分消费者挂掉,当重新连接时,丢失的消息无法获得;Redis宕机,PubSub的消息不会持久化。
Redis5.0提供Stream数据结构,提供持久化消息队列。(后续有介绍)

2.7 小对象压缩

如果Redis内部管理的集合数据结构很小,会使用紧凑存储形式压缩存储。
当hash数据较小时,使用ziplist存储,key和value作为两个相邻的entity存储。
当zset数据较小时,使用ziplist存储,value和score作为两个相邻的entity存储。
当set元素都是整数且元素个数较少时使用intset紧凑型整数数组结构存储。如果set存储的是字符串,会立刻升级为hashtable结构。
当集合元素增加或者某个value值过大时,小对象存储会被升级为标准结构。
内存回收机制
删除key后并不是马上被回收,操作系统以页为单位来进行回收,只要页上有一个key在使用,就不能被回收,虽然已删除的key未被立即回收,Redis会重新使用那些尚未被回收的空闲内存。
执行flushdb,所有key都被干掉了,大部分之前的页面都完全干净了,就会立刻被操作系统回收。

第三篇 集群篇

3.1 主从同步

CAP原理
C:Consistent,一致性
A:Availability,可用性
P:Partiton tolerance,分区容忍性
当网络分区发生时,一致性和可用性两难全。
Redis主从数据是异步同步,分布式Redis不满足一致性,满足可用性。保证最终一致性,从节点发生故障重启后,会努力追赶主节点,最终从节点和主节点状态保持一致。
主从同步、从从同步
增量同步:主节点将自己状态产生修改的指令记录到本地内存buffer中,异步将buffer的指令同步到从节点,从节点一边同步指令流一边向从节点反馈同步偏移量。主节点的buffer是一个定长环形数组,如果数组满了会覆盖前面内容。如果从节点还没有同步指令就被覆盖了,将触发快照同步。
快照同步:在主节点先进行一次bgsave,将当前内存数据全部快照到磁盘文件,再将快照文件同步给从节点。从节点接受完快照文件后,开始进行一次全量同步,加载前清空数据库,加载完成后继续进行增量同步。
增加从节点:当从节点加入集群时,先进行一次快照同步,完成后再进行一次增量同步。
无盘复制:主节点快照同步时需要将内存文件写到本地磁盘,消耗性能,Redis支持无盘复制,也就是主服务器直接通过套接字将快照内容发送到从节点,主节点一边遍历内存,一边将序列化的内容发送到从节点,从节点将收到的内容存储到磁盘中,然后再进行一次性加载。
wait指令可以使异步复制变成同步复制。

3.2 Sentinel

主从保证最终一致性,sentinel提供高可用,发生故障时可以自动进行主从切换。
监控主从节点健康,当主节点挂掉时,自动选择最有从节点升级为主节点。客户端连接集群时先连接sentinel,通过sentinel获取主节点地址,然后与主节点进行交互。当主节点发生故障时,客户端会重新向sentinel获取新的主节点地址。
sentinel无法保证消息完全不丢失。
主节点断了,连接池建立连接时会查询主节点地址是否与内存中地址一致,如果变更,则断开所有连接,重新使用新地址。
主动切换主节点,主节点所有修改下执行会抛出ReadonlyError。客户端将重新获取主节点地址,重新连接。

3.3 Codis

Codis是一个代理中间件。负责将特定的key转发到指定的Redis实例。
Codis默认将所有key划分成1024个槽(slot),首先对客户端传过来的key进行crc32运算计算hash值,再将hash后的整数对1024取模得余数,余数就是对应槽位。
走代理,网络开销要大一点。数据在多个redis中,无法支持事务操作,rename也比较危险。

3.4 Cluster

去中心化,客户端需要缓存槽位相关信息。直接定位key所在节点。
通过Gossip协议来刚播自己得状态以及改变对整个集群的认知。

第四篇

4.1 Stream

Redis5.0推出Stream,支持多播的可持久化消息队列。
每个Stream可以挂多个消费组,每个消费组有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费已经消费到哪条消息。消费组通过xgroup create创建。每个消费组是独立的,也就是每条消息都会被每个消费组消费到。同一个消费组内可以挂接多个消费组,组内的消费组是竞争关系。

4.2 Info指令

通过Info指令了解Redis运行状态,分为9大块:

Server:服务器运行的环境参数。
Clients:客户端相关信息。
Memory:服务器运行内存统计数据。
Persistence:持久化信息。
Stats:通用统计数据。
Replication:主从复制相关信息。
CPU:CPU使用情况。
Cluster:集群信息。
KeySpace:键值对统计数量信息。

Info可以一次性获取所有信息,也可以按快获取信息。

>info  # 获取所有信息
>info memory  # 获取内存相关信息
>info replication   # 获取主从复制相关信息

4.3 再谈分布式锁

Redlock:
加锁时,会向过半节点发送set(key,value,nv=True,ex=xxx)指令,只要过半节点set成功,就认为加锁成功。释放锁时,需要向所有节点发送del指令。
需要向多个节点进行读写,性能比单Redis下降一点。

4.4 过期策略

Redis将每个设置了过期时间的key放到一个独立的字典中,会定时遍历这个字典来删除过期的key。同时,还是用惰性策略删除key,也就是访问key时key过期了就删除key。
定时扫描策略:
每秒进行10次过期扫描,采取贪心策略:

从过期字典中随机选出20个key;
删除这20个key中已过期的key;
如果过期的key的比例超过1/4,那就重复1)。

Redis会持续扫描过期字典(循环多次),知道过期字典的key变得稀疏,才会停止。这就会导致线上读写请求卡顿。导致卡顿还有一个愿意,就是内存管理器需要频繁回收内存也,也会产生一定的CPU消耗。
当客户请求到来时,如果正好服务器进入过期扫描状态,客户端请求等待25ms之后才会进行处理,而客户端将超时时间设置得比较短,那就会出现大量链接因超时而关闭。
如果有大批量key过期,需要给过期的key设置一个随即范围。
从节点不会进行过期扫描。主节点在key到期时会在AOF文件中增加一条del指令同步到从节点,从节点通过执行这条指令来删除过期的key。

4.5 LRU

当实际内存超过maxmemory时,Redis提供几种可选策略:

noevction:不会继续服务写请求(del请求可以继续服务),读请求可以继续服务。默认淘汰策略。
volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先淘汰。
volatile-ttl:尝试淘汰设置了过期时间的key,最少剩余寿命ttl优先淘汰。
volatile-random:尝试淘汰设置了过期时间的key,随机。
allkeys-lru:尝试淘汰所有key中最少使用的key。
allkeys-random:尝试在所有key中随机淘汰。

Redis使用的近似LRU算法
当Redis执行写操作时,发现内存超出maxmemory,就进行一次LRU淘汰算法。随机采样5(可配置)个key,谈话淘汰最旧的key,淘汰之后还超出maxmemory,继续随机采样淘汰,直到低于maxmemory。

4.6 懒惰删除

Redis并不是只有一个主线程,还有几个异步线程用于处理一些耗时的操作。
del一个大key时,可能会卡顿,Redis4.0引入unlink指令,对删除操作进行懒处理,丢给后台线程异步回收内存,如果key很小,和del一样,会立刻回收。
flushdb和flushall也是非常慢的操作,Redis4.0提供了异步化,在这两个指令后面增加async参数即可。
异步队列:主线程将对象引用从”大树“中摘除后,会将这个key的内存回收操作包装成一个任务,塞进异步任务队列,后台线程会从这个异步队列中取任务。
AOF日志的sync函数也比较耗时,Redis将这个操作移步到异步线程来完成。有一个独立的处理AOF Sync操作的异步线程。

4.7 优雅的使用Jedis

4.8 保护Redis

可重命名指令防止入侵

>rename-command keys abc
>rename-command flushall ”“

增加密码访问限制

requirepass 123455
masterauth 123456  

4.9 Redis安全通信

第五篇 源码篇

5.1 探索“字符串”内部

5.2 探索“字典”内部

5.3 探索“压缩列表”内部

5.4 探索“快速列表”内部

5.6 探索“紧凑列表”内部

5.7 探索“基数树”内部

5.8 LFU VS LRU

5.9 懒惰删除的巨大牺牲

5.10 深入字典遍历

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值