redis核心原理与设计思想

  • 一、redis的5种基本数据结构
    • 1、String(字符串)
      • redis字符串扩容策略
    • 2、list(列表)
      • list常用命令
      • 右边进左边出:队列
      • 右边进右边出:栈
      • 快速列表
    • 3、hash(字典)
      • 渐进式rehash
    • 4、set(集合)
    • 5、zset(有序集合)
      • zset的应用
      • 跳跃列表
    • 6、Redis容器型数据结构的通用规则
    • 7、过期时间
    • 8、redis分布式锁和锁超时问题
      • 超时问题
    • 9、redis分布式锁的可重入性
    • 10、redis来做延时队列
      • 10.1 redis做异步消息队列
      • 10.2、队列空了怎么办
        • 阻塞读
        • 空连接自动断开
    • 11、Redis分布式锁冲突处理
      • 直接抛出异常,通知用户稍后重试
      • sleep一会儿,然后重试
      • 将请求转移到延时队列,过一会再试
    • 12、延时队列的实现
  • 二、Redis的高级数据结构(5种)
    • 1、位图bitMap(打卡签到神器)
      • 位图的基本用法
        • 统计星期一用户的打卡量
    • 2、HyperLogLog(去重计数器,用来统计uv)
      • HyperLogLog数据结构简介
      • 使用方法
      • pfadd 这个 pf 是什么意思?
      • pfmerge 适合场景
      • HyperLogLog注意事项
      • HyperLogLog内存占用为什么是 12k
    • 3、布隆过滤器(Bloom Filter)
      • 啥是布隆过滤器
      • Redis的布隆过滤器插件
      • 布隆过滤器的基本用法
      • 布隆过滤器的原理
    • 4、Redis-Cell(天然的分布式限流模块)
      • 4.1、令牌桶限流算法原理及步骤
      • 4.2、定义一个令牌桶,其拥有几个关键属性
      • 4.3、从桶中申请令牌,这一步中有两个关键动作
      • 4.4、Redis-cell用法
      • 4.5、限流示例:
    • 5、GeoHash(经纬度计算地理附近的人)
      • 用postgreSql来计算
      • Geo指令的基本用法
        • geoadd指令(增加)
        • geodist指令(距离)
        • georadiusbymember指令(查询附近的妹子)
      • Redis Geo使用时的注意事项
  • 三、redis的基本原理和操作
    • 1、scan命令
    • 2、scan基本用法
    • 3、字典的结构
    • 3.1、字典内部结构图--->字典的扩容
    • 3.2、字典扩容--->rehash过程
    • 3.3、渐进式rehash
    • 4、在平时业务开发中,避免大key的产生
    • 5、redis的线程I/O模型
      • 5.1、为何redis单进程、单线程还可以处理高并发?
    • 6、Redis的通信协议
    • 7、Redis的持久化
      • Redis4.0的混合持久化
    • 7.1、rdb的快照原理
      • 鸡贼的rdb快照过程
      • RDB持久化配置
    • 7.2、AOF的日志备份原理
      • AOF持久化配置
    • 7.3、AOF日志VS快照rdb
    • 8、管道(Pipeline)
  • 四、redis的高级原理和机制
    • 1、redis事务
    • 1.1 discard(丢弃)
    • 1.2 watch机制
      • 在 Redis 中使用 watch 命令可以决定事务是执行还是回滚
        • Redis watch流程图:
      • 使用watch成功提交的事务的案例
      • 使用watch回滚的事务的案例
    • 1.3 watch注意事项
    • 2、pubsub(消息的订阅和发布)
    • 3、Redis的内存回收机制
      • 前置问题:
      • 还有两个更神奇的问题:
      • 3.1、删除达到过期时间的键对象
        • 定时任务删除
        • 惰性删除
      • 3.2、内存使用达到 max memory 上限时触发内存溢出控制策略
    • 4、redis的过期策略
    • 5、LFU VS LRU
  • 五、redis的集群和高可用方式
    • 1、CAP原理
  • 六、redis5.0新特性
    • 1、Stream(有Ack机制的消息队列)

一、redis的5种基本数据结构

Redis是“Remote Dictionary Service”(远程字典服务)的缩写。

Redis提供5种基础数据结构,分别是:String、list、hash、set、zset。
Redis所有的数据结构都以唯一的key字符串作为名称,然后通过操作唯一key值来获取相应的value数据。
不同的数据结构的差异在于value的结构不一样。

1、String(字符串)

字符串String指的是redis的value值的数据结构是字符串,它的内部表示就是一个字符数组。
下图为一个字符数组:

len: 表示字符串的真正长度(不包含NULL结束符在内)。
alloc: 表示字符串的最大容量(不包含最后多余的那个字节)。
flags: 总是占用一个字节。其中的最低3个bit用来表示header的类型。
buf: 字符数组。

redis的字符串是动态字符串,是可以修改的字符串。内部结构实现实现类似Java的ArrayList,
采用预分配冗余空间的方式来减少内存的频繁分配。
如图:

redis内部为当前字符串分配的实际空间capacity一般要高于实际字符串的长度len。当空间不足时,开始扩容。

redis字符串扩容策略

1、当字符串的长度小于1MB时,扩容是以加倍现有的空间。
2、当字符串大于1MB时,扩容时一次只会扩1MB的空间。
注意:字符串的最大长度是512M。

2、list(列表)

Redis的列表相当于Java里面的LinkedList,注意一点list是链表而不是数组。
list的插入和删除操作非常快,时间复杂度度是O(1),
但索引定位很慢,时间复杂度是O(n),
如图所示,列表中每个元素使用双向指针,串起来可以同时支持前后遍历。

Redis的list结构常用来做异步队列。将需要异步处理的数据,塞进Redis的list列表,另一个线程负责从这个列表中读取数据进行处理。

list常用命令

rpush 从右边添加数据
lpush  从左边添加数据rpop 从右边删除该key对应列表中的第一个元素(右边第一个)
lpop 从左边删除该key对应列表中的第一个元素(左边第一个)

for example:

用两种方式的添加命令lpush list1 a b c d →结果      d c b arpush list a b c d    →结果     a b c d 原因是:从左边添加数据,已添加的需向右移从右边添加数据,已添加的向左移

右边进左边出:队列

队列是先进先出的数据结构,常用于消息排队和异步逻辑处理,它会确保元素的访问顺序性。

右边进右边出:栈

栈是先进后出的数据结构,跟队列恰好相反。用Redis做栈的业务场景不多。

快速列表

深入Redis源码,发现Redis的底层存储并不是一个简单的linkedList,而是称为“快速列表”(quicklist)的一个结构。

首先,在列表元素较少的情况下,会使用一块连续的内存存储,这个结构是ziplist,即压缩列表。它将所有的元素彼此紧挨着一起存储,分配一块连续的内存。

当数据量比较大的时候ziplist才会变成quicklist。所以Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。

  quickList 是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。

3、hash(字典)

Redis的字典相当于Java中的hashMap,实现结构上与Java7的hashmap也是一样的,都是“数组+链表”二维结构。

redis的hash架构就是标准的hashtab的结构,通过挂链解决冲突问题。第一维hash的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。

不同的是,Redis的字典的值只能是字符串,另外它们的rehash的方式不一样。
Java的hashmap在字典很大时,rehash是个耗时的操作,需要一次性全部rehash。redis为了追求性能,不能堵塞服务,所以采用了渐进式rehash的策略。

渐进式rehash

渐进式rehash会在rehash的同时,保留新旧两个hash结构,查询时会同时查询这两个hash结构,然后在后续的定时任务中,循序渐进的将旧hash的内容一点点迁移到新的hash结构中。当搬迁完成后,就会使用新的hash结构取而代之。
当hash移除了最后一个元素之后,该数据结构会被自动删除,内存被回收。

hash结构很适合存储Java中对象信息,如用户信息。
但hash结构也有缺点,hash结构的存储消耗要高于单个字符串。到底使用hash还是字符串,要根据实际情况来权衡。

4、set(集合)

redis的集合相当于Java里的hashset,它内部的键值对是无序的、唯一的。

当集合中最后一个元素被移除后,数据结构被自动删除,内存被回收。

set结构可以用来存储某活动中中奖的用户ID,因为它有去重功能,保证一个用户不会中奖2次。

5、zset(有序集合)

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

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

zset的应用

zset可以用来存储粉丝列表,value值为粉丝用户id,score是关注事件,我们可以对粉丝列表按照关注时间排序。
zset还可以用来存储学生的成绩,value值是学生的id,score是他的考试成绩。

跳跃列表

zset的内部实现用的是“跳跃列表”的数据结构,它的结构非常特殊,也很复杂。
因为zset要支持随机的插入和删除,那么它就不适合用数组来表示。
关于跳跃列表的问题,可以后续内容详细介绍。

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

redis的list、set、hash、zset这四种数据结构是容器型数据结构,它们共享下面两条通用规则。

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

7、过期时间

Redis的所有数据结构都可以设置过期时间,时间到了,Redis会自动删除过期对象。

需要注意:过期是以对象为单位,比如一个hash结构的过期是整个hash对象的过期,而不是其中某个key的过期。时间到了,Redis会自动删除过期对象。注意是删除对象,但是不一定会立即回收内存。
只有容器内没有元素了,才会立即回收内存,正如规则 drop if no elements。

8、redis分布式锁和锁超时问题

在 Redis 里,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

SETNX key value将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。

Redis的2.8版本以后,将set指令扩展为setnx和expire指令一起执行,彻底解决分布式锁的乱象。

SET mykey “redis” EX 60 NXShell以上示例将在键“mykey”不存在时,设置键的值,到期时间为60秒。

超时问题

Redis分布式锁不能解决超时问题,如果加锁和释放锁之间的业务逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。
为了避免这个问题,Redis分布式锁不要用于较长时间的任务。

9、redis分布式锁的可重入性

可重入性:指线程在持有锁的情况下再次请求加锁;
如果一个锁支持同一个线程多次加锁,那么这个锁就是可重入的。比如Java中的ReentrantLock就是可重入锁。
Redis分布式锁如果要支持可重入性,需要客户端的set方法进行包装,使用线程的Treadlocal变量存储当前持有锁的计数。
不过不推荐redis使用可重入锁,它加重了客户端的复杂性,在编写业务代码的时候主义在逻辑结构上调整,完全可以不使用可重入锁。

10、redis来做延时队列

需要注意的是:Redis的消息队列不是专业的消息队列,它缺乏很多的高级特性,没有ack机制,如果对消息的可靠性有着极高的要求,那么它就不适合用。推荐rabbitMQ。

10.1 redis做异步消息队列

Redis的list(列表)数据结构常用来作为异步消息队列使用,常用右进左出来实现,rpush和lpop。当然也可以用左进右出,道理一样。

10.2、队列空了怎么办

客户端通过队列的pop操作来获取消息,可是队列空了,客户端就会陷入pop的死循环,不停地pop,没有数据,接着再pop,还没有数据。这是浪费生命的空轮询。
空轮询不但拉高了客户端的CPU,Redis的QPS也会被拉高,如果这样的空轮询客户端有几十个,Redis的慢查询可能会显著增多。

解决途径:我们通常使用sleep来解决这个问题,让线程睡一会,睡个1s就可以了。这时候客户端的cpu下来了,Redis的QPS也降下来了。

上面的睡眠可以解决问题,但是又有个小问题?那就是睡眠会导致消息的延迟增大。
如果有1个消费者,那么久延迟1秒。如果有多个消费者呢?

有什么办法能显著降低延迟呢?

把睡眠的时间缩短,这个方法当然也行。不过有更好的解决方案:
那就是使用blpop和brpop。两个指令前的b代表blocking,也就是“阻塞读”。

阻塞读

阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立即醒过来。消息的延迟几乎为零。
用blpop和brpop代替前面的lpop/rpop,就完美解决了上面的问题。

空连接自动断开

阻塞读并不能完美解决问题,其实还有一个问题需要解决。
答案是:空闲连接的问题。
如果线程一直阻塞在那里,Redis的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候blpop/brpop就会抛出异常。
所以编写客户端业务代码要小心,如果捕获到异常,还要重试。

11、Redis分布式锁冲突处理

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

1、直接抛出异常,通知用户稍后重试。
2、sleep一会儿,然后重试。
3、将请求转移到延时队列,过一会再试。

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

这种方式适用于用户直接发出的请求。它本质上是对当前请求的放弃,由用户决定是否重新发起请求。

sleep一会儿,然后重试

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

将请求转移到延时队列,过一会再试

这种方式比较适合异步消息处理,将当前冲突的请求扔到另一个队列延后处理以避开冲突。

12、延时队列的实现

延时队列通过Redis的zset(有序列表)来实现。

我们将消息序列化成一个字符串作为zset的value,这个消息的到期处理事件作为score,然后用多个线程轮询zset获取到期的任务进行处理。

具体代码实现可以Google一下。

二、Redis的高级数据结构(5种)

1、位图bitMap(打卡签到神器)

在平时开发中,总有一些Boolean类型的数据需要存取。比如上班打卡,签到是1,没签是0,要记录365天。如果要使用key/value,每个用户要记录365个数据。当用户上亿的时候,需要的存储空间是惊人的。如利用企业微信上班打卡签到。
为了解决这个问题,Redis提供了位图数据结构。

for example:
构造一个位图,里面存的是二进制数据,如:1 0 1 0 1 0 1,通过修改userId对应位置上的0和1来修改用户上班打卡状态,由于默认值为0,所以1代表用户处于已打卡状态,0代表用户处于旷工状态,如图:


构造了Mon、Thus、Web三个位图,
对于Mon来说,userId=1的用户处于打卡状态,userId=2的用户处于旷工状态,userId=3的用户处于已打卡状态,
当userId=10的用户上班打卡后,就把第10位上值变成1

位图的基本用法

首先说一下,位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是byte数组。
我们使用普通的set/get直接获取位图的内容,也可以使用位图操作getbit/setbit等将byte数组看成“位数组”来处理。

举个例子:

第一天
userId=10000,userId=9999,userId=8888的用户上班打卡了setbit mon 10000 1;
setbit mon 9999 1;
setbit mon 8888 1;

统计星期一用户的打卡量

bitcount mon

返回:

2、HyperLogLog(去重计数器,用来统计uv)

HyperLogLog数据结构是Redis的高级数据结构,它适用于统计网站的UV。

例如:我们在业务开发中统计PV,非常好办,给给个网页分配一个独立的Redis计数器,再把这个计数器的后缀加上当天的日期。这样来一个请求,执行incrby指令一次,就可以算出来PV的数据。

但是UV不一样,它要去重,每个网页都要带上用户的ID,无论登录用户还是未登录用户都需要一个唯一id来标识。
也许你可以用set集合去做,使用sadd将用户id塞进去。然后用scard可以获取这个集合的大小,这个数字就是UV的数据。
如果页面的用户量访问比较大,可能上千万或过亿,就需要一个很大的set集合来统计,这就非常浪费存储空间。而且这个存储空间是惊人的!
其实老板所需要的数据并不太精确,200万和201万这两个数字对老板来说没有多大区别。那么,这时有更好的解决方案!
Redis提供HyperLogLog数据结构来解决这种问题。

HyperLogLog数据结构简介

HyperLogLog提供不精确去重统计方案,虽然不精确,但也不离谱,标准误差0.81%。

使用方法

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

pfadd 这个 pf 是什么意思?

HyperLogLog 这个数据结构的发明人 Philippe Flajolet 的首字母缩写,发型很骚,看起来是个佛系教授。

pfmerge 适合场景

HyperLogLog 除了 pfadd 和 pfcount 之外,还提供了一个指令 pfmerge,
用于将多个 pf 计数值累加在一起形成一个新的 pf 值。

HyperLogLog注意事项

HyperLogLog 它需要占据一定 12k 的存储空间,所以HyperLogLog不适合统计单个用户相关的数据。

HyperLogLog内存占用为什么是 12k

Redis 的 HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,
最大可以表示 maxbits=63,于是总共占用内存就是2^14 * 6 / 8 = 12k字节。

3、布隆过滤器(Bloom Filter)

Redis的高级数据结构布隆过滤器(Bloom Filter),它是专门用来解决去重问题的。只是有些不那么精确,也就是一定的误判概率。

在HyperLogLog中,只提供了pfadd和pfcount方法,没有提供pfcontains方法。
讲个业务场景,比如今日头条的推荐系统,它不停地给我们推荐新内容,每次推荐都会去重,去掉我们以前看过的内容。那么字节跳动是如何实现的?

啥是布隆过滤器

布隆过滤器可以理解成一个不怎么精确的set结构,当你使用contains方法判断某个对象是否存在的时候,它可能会误判。
当布隆过滤器说某个值存在时,这个值可能不存在;
当说某个值不存在时,这个值一定不存在。

所以在业务场景中,布隆过滤器常用于过滤用户已经浏览过的页面,这样就可以保证用户推荐的内容是不重复的。

布隆过滤器对已经见过的元素绝对不会误判,它只会误判那些没有见过的元素。

Redis的布隆过滤器插件

Redis实在Redis4.0版本之后bloomfilter才正式作为一个插件加载到Redis Server中。

布隆过滤器的基本用法

布隆过滤器有两个基本指令,bf.add和bf.exists
bf.add添加元素,
bf.exists 查询元素是否存在
bf.madd 一次可以添加1个或多个元素
bf.mexists 一次查询多个元素是否存在
注意:
布隆过滤器的bf.add只能一次添加1个元素

布隆过滤器的原理

参考google guava实现的布隆过滤器
布隆过滤使用不当,掉坑里,架构师能怼死你

4、Redis-Cell(天然的分布式限流模块)

Redis4.0版本以后,提供了Redis-Cell,专门用于请求限流。
该模块使用了漏斗算法,并提供了原子的限流指令。

4.1、令牌桶限流算法原理及步骤

令牌桶算法的原理是定义一个按一定速率产生token的桶,每次去桶中申请token,若桶中没有足够的token则申请失败,否则成功。

在请求不多的情况下,桶中的token基本会饱和,此时若流量激增,并不会马上拒绝请求,所以这种算法允许一定的流量激增。

4.2、定义一个令牌桶,其拥有几个关键属性

桶容量
令牌产生速率
当前桶中令牌数
最近一次取(生成)令牌时间

4.3、从桶中申请令牌,这一步中有两个关键动作

根据上一次生成令牌时间到现在的时间,及生成速率计算出当前令牌桶中的令牌数判断令牌桶中是否有足够的令牌,并返回结果

这几个步骤可以采用redis提供的原生命令去实现,但是,但是,但是高并发的时候数据会不一致,所以 redis-cell 将这个过程原子化,完美解决了分布式环境下数据的一致性问题。

4.4、Redis-cell用法

该模块只提供了一个命令:

CL.THROTTLE

for example:

CL.THROTTLE test 100 400 60 3

参数说明

test: redis key100: 官方叫max_burst,没理解什么意思,其值为令牌桶的容量 - 1, 首次执行时令牌桶会默认填满400: 与下一个参数一起,表示在指定时间窗口内允许访问的次数60: 指定的时间窗口,单位:秒3: 表示本次要申请的令牌数,不写则默认为 1

以上命令表示从一个初始值为100的令牌桶中取3个令牌,该令牌桶的速率限制为400次/60秒。

127.0.0.1:6379> CL.THROTTLE test 100 400 60 3
1) (integer) 0
2) (integer) 101
3) (integer) 98
4) (integer) -1
5) (integer) 0

返回值说明:

1: 是否成功,0:成功,1:拒绝2: 令牌桶的容量,大小为初始值+13: 当前令牌桶中可用的令牌4: 若请求被拒绝,这个值表示多久后才令牌桶中会重新添加令牌,单位:秒,可以作为重试时间5: 表示多久后令牌桶中的令牌会存满

4.5、限流示例:

 下面以一个速率稍慢一点的令牌桶来演示一下,连续快速执行以下命令:
127.0.0.1:6379> CL.THROTTLE test1 10 5 60 3
1) (integer) 0
2) (integer) 11
3) (integer) 8
4) (integer) -1
5) (integer) 36
127.0.0.1:6379> CL.THROTTLE test1 10 5 60 3
1) (integer) 0
2) (integer) 11
3) (integer) 5
4) (integer) -1
5) (integer) 71
127.0.0.1:6379> CL.THROTTLE test1 10 5 60 3
1) (integer) 0
2) (integer) 11
3) (integer) 2
4) (integer) -1
5) (integer) 106
127.0.0.1:6379> CL.THROTTLE test1 10 5 60 3
1) (integer) 1
2) (integer) 11
3) (integer) 2
4) (integer) 10
5) (integer) 106

通过命令可以看到,每次从桶中取出3个令牌,当桶中令牌不足时,请求被拒绝。

5、GeoHash(经纬度计算地理附近的人)

Redis在3.2版本以后增加地理位置Geo模块,例如利用Redis实现类似摩拜单车“附近的Mobike”、美团的“附近的餐馆”功能。

用postgreSql来计算

效率低、慢。

Geo指令的基本用法

Redis提供的Geo指令只有6个。本质上是一个zset结构。

geoadd指令(增加)

geoadd指令携带集合名称以及多个经纬度、名称的三元数组。
添加单一地点:

GEOADD beijing-area 116.2161254883 39.8865577059 shijingshan 

添加多个地点:

geoadd Beijing-areas 116.2161254883 39.8865577059 ShiJingShan 116.1611938477 39.7283134103 FangShan 116.3534545898 39.7071866568 DaXing 116.4166259766 39.9097362345 DongChenQu

geodist指令(距离)

geodist指令用来计算两个元素之间的距离,携带集合名称、两个名称和距离单位,单位的距离有m米km千米等

192.168.1.130:6379> geodist Beijing-areas ShiJingShan FangShan m"18216.0860"

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) 40698860083613984) 1) "116.5142020583152771"2) "39.90540918662494363"
2) 1) "juejin"2) "10.5501"3) (integer) 40698871543881674) 1) "116.48104995489120483"2) "39.99679348858259686"
3) 1) "meituan"2) "11.5748"3) (integer) 40698871790834784) 1) "116.48903220891952515"2) "40.00766997707732031"...

Redis Geo使用时的注意事项

在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。 在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。
所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。

如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。(注意:zset集合大小,进行合适地切分)

三、redis的基本原理和操作

1、scan命令

生产环境中Redis的运维工作,有时候要从Redis的实例中成千上万key中找出特定前缀的key列表来手动处理数据。
熟悉Redis的人都知道,它是单线程的。因此在使用一些时间复杂度为O(N)的命令时要非常谨慎。可能一不小心就会阻塞进程,导致Redis出现卡顿。

在Redis2.8版本之前,我们可以使用keys命令按照正则匹配得到我们需要的key。但是这个命令有两个缺点:

1、没有offset、limit,我们只能一次性获取所有符合条件的key,如果结果有上百万条,那么等待你的就是“无穷无尽”的字符串输出。2、keys命令是遍历算法,时间复杂度是O(N)。如我们刚才所说,这个命令非常容易导致Redis服务卡顿。因此,我们要尽量避免在生产环境使用该命令。

相比于keys命令,scan命令有两个比较明显的优势:

scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。scan命令提供了limit参数,可以控制每次返回结果的最大条数。同keys一样,它提供模式匹配功能。

2、scan基本用法

SCAN相关命令包括SSCAN 命令、HSCAN 命令和 ZSCAN 命令,分别用于集合、哈希键及有续集等

SCAN 命令用于迭代当前数据库中的数据库键。
SSCAN 命令用于迭代集合键中的元素。
HSCAN 命令用于迭代哈希键中的键值对。
ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

因为 SCAN 、 SSCAN 、 HSCAN 和 ZSCAN 四个命令的工作方式都非常相似, 要记住:

SSCAN 命令、 HSCAN 命令和 ZSCAN 命令的第一个参数总是一个数据库键。而 SCAN 命令则不需要在第一个参数提供任何数据库键 —— 因为它迭代的是当前数据库中的所有数据库键

命令格式:

SCAN cursor [MATCH pattern] [COUNT count]命令解释:scan 游标 MATCH <返回和给定模式相匹配的元素> count 每次迭代所返回的元素数量

示例:

scan 0 match DL* count 5 

3、字典的结构

在redis中所有的key都存在一个很大的字典中,这个字典结构和Java中的hashMap一样。它是一维是数组,二维链表的结构。

所以,字典数据结构的精华就落在了 hashtable 结构上了。hashtable 的结构和 Java 的 HashMap 几乎是一样的,都是通过分桶的方式解决 hash 冲突。
第一 维是数组,第二维是链表。数组中存储的是第二维链表的第一个元素的指针。

3.1、字典内部结构图—>字典的扩容


dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。
但是在 dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,
这 时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。
待搬迁 结束后,旧的 hashtable 被删除,新的 hashtable 取而代之。

3.2、字典扩容—>rehash过程

rehash:(即重新进行hash计算)
rehash就是将元素的hash值对数组长度重新取模计算,因为长度变了,每个元素挂接的槽位可能发生变化。

3.3、渐进式rehash

Java的hashmap在扩容时会一次性将旧数据的元素全部转移到新数组下面。如果Hashmap中元素特别多,线程会出现卡顿。
Redis为了解决这个问题,采用“渐进式rehash”。
如3.1图中所示,它会保留新数组和旧数组,然后在定时任务中以及后续hash的指令操作中逐渐将旧数组的数据迁移到新数组中
这意味着操作rehash中的字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到,还需要去新数组中去找。

4、在平时业务开发中,避免大key的产生

如果在业务开发中监控到Redis的内存大起大落,极有可能是因为大key导致的。
如何定位大key?
用如下命令:

redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1这个指令每隔100条scan指令就会休眠0.1秒,不会一起QPS震动

5、redis的线程I/O模型

Redis是个单进程、单线程的程序!
所以小心使用redis,对于时间复杂度为O(n)级别的指令,一定要慎重使用。如 keys,否则一不小心就会导致Redis卡顿,甚至挂掉进程。

5.1、为何redis单进程、单线程还可以处理高并发?

1、redis所有数据都在内存中,所有运算都是内存级别的运算。
2、采用非阻塞I/O,多路复用。
3、指令队列(请求队列),客户端的指令通过队列来进行顺序处理,先到先服务。
4、响应队列,Redis服务端通过响应的队列将指令结果回复客户端。
5、定时任务,如果线程阻塞在select系统的调用上,可以给select设置一个timeout的参数。Nginx和Node.js的事件处理原理和Redis相似。
多路复用的2种实现:

Linux系统的【事件轮询API】是select()函数,它是操作系统提供给用户程序的API,又称为“多路复用API”。现代的Linux操作系统的多路复用API已经不再使用select系统调用,而改用epoll【linux】和kqueue【MacOs】。

科普一下:
事件轮询API就是Java中的NIO技术,而epoll叫NIO2.0,也成epoll为AIO。
NIO是同步非阻塞IO,AIO是异步非阻塞IO.

6、Redis的通信协议

数据库系统的瓶颈一般不在于网络流量,而在于数据库自身的逻辑处理上。
即使Redis采用了浪费流量的文本协议,依然可以取得极高的性能。
单个节点单个cpu核心QPS达到10W/s。

Redis服务器与客户端通过RESP(REdis Serialization Protocol)协议通信。它是一种直观的文本协议,优势在于实现异常简单,解析性能极好。
RESP 协议的简单性、易理解性和易实现性,使它成为互联网技术领域非常受欢迎的一个文本协议。有很多开源项目使用 RESP 作为它的通讯协议。

7、Redis的持久化

为防止突然宕机,redis提供了持久化机制。
redis的持久化机制有2种:(rdb快照和aof日志)
rdb快照:
快照是一次全量备份。
AOF日志
日志是连续的增量备份。

注意:需要定期给AOF进行重写,给AOF日志瘦身。因为AOF日志在长期的运行过程会变得无比庞大,redis实例在重启的时候需要加载AOF日志进行指令重放,这个时间会无比漫长。

通常redis的主节点不会进行任何持久化的操作,持久化都是在从节点进行的。

Redis4.0的混合持久化

重启redis时,很少使用rdb来恢复数据,因为它会丢失大量数据。我们通常使用AOF重放,但AOF重放日志相对rdb又慢许多。
为了解决这个问题,Redis4.0才会混合方式。
这里的AOF日志不再是全量的日志,而是自持久化开始到持久化结束这一小段时间内的增量AOF日志,通常这部分AOF日志很小。
于是在Redis重启的时候,可以先加载rdb快照,然后在重放增量AOF日志,就可以完全代替原来的AOF全量文件重放,重启的效率大幅提升。

7.1、rdb的快照原理

原理是将Reids在内存中的数据库记录定时 dump到磁盘上的RDB持久化。

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

鸡贼的rdb快照过程

内存快照要求Redis必须进行文件IO操作,可文件IO操作不能使用多路复用API。(多路复用API只能用于网络IO操作)

这意味着单进程单线程的Redis在服务线上请求的同时,还要进行IO操作,而文件IO操作会严重拖累Redis服务器的性能。

还有一个问题,为了不阻塞线上的业务,Redis需要一边持久化,一边响应客户端的请求。持久化的同时,内存数据结构还在改变,例如一个大型的hash正在持久化,结果客户端一个指令把它删掉了,可是还没有持久化完呢,该怎么办呢?

Redis使用操作系统的多进程COW(Copy on Write)机制来实现快照的持久化。多进程COW也是鉴定程序员知识广度一个重要标志。

RDB持久化配置

Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:


save 900 1              #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。save 300 10            #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。

7.2、AOF的日志备份原理

AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

AOF持久化配置

在Redis的配置文件中存在三种同步方式,它们分别是:


appendfsync always     #每次有数据修改发生时都会写入AOF文件。appendfsync everysec  #每秒钟同步一次,该策略为AOF的缺省策略。appendfsync no          #从不同步。高效但是数据不会被持久化。

7.3、AOF日志VS快照rdb

在线上我们到底该怎么做?我提供一些自己的实践经验。

如果Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回;自己制定策略定期检查Redis的情况,然后可以手动触发备份、重写数据;单机如果部署多个实例,要防止多个机器同时运行持久化、重写操作,防止出现内存、CPU、IO资源竞争,让持久化变为串行;可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令;RDB持久化与AOF持久化可以同时存在,配合使用。

8、管道(Pipeline)

管道(Pipeline)本身并不是redis服务器提供的技术,这个技术本身是客户端提供的,跟服务器没有什么关系。
概括起来一句话:一次性发送多条指令,结果一次性返回。

管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,
pipeline通过减少客户端与redis的通信次数来实现降低往返延时时间,
而且Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。

四、redis的高级原理和机制

1、redis事务

在mysql中,我们用begin、commit和rollback来操作事务。
而在Redis中,我们用multi、exec、discard

以下是一个事务的例子, 它先以 MULTI开始一个事务,
然后将多个命令入队到事务中,
最后由EXEC 命令触发事务, 一并执行事务中的所有命令:

redis> MULTI
OKredis> SET book-name "Mastering C++ in 21 days"
QUEUEDredis> GET book-name
QUEUEDredis> SADD tag "C++" "Programming" "Mastering Series"
QUEUEDredis> SMEMBERS tag
QUEUEDredis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"2) "C++"3) "Programming"

1.1 discard(丢弃)

Redis为事务提供一个discard指令,用于丢弃事务缓存队列中的所有指令,在exec执行之前。

Redis可没有事务的回滚机制,只有一个丢弃机制。
(但是可以用watch机制做个乐观锁实现回滚机制)

DISCARD取消事务:127.0.0.1:6379> set k1 k1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get k1
"k1"
127.0.0.1:6379>

在discard之后,队列中的所有指令都没执行。

1.2 watch机制

分布式锁是一种悲观锁,会出现锁冲突。Redis 参考了Java的juc下多线程中使用的 CAS (比较与交换, Compare And Swap ) 去执行的。在数据高并发环境的操作中,我们把这样的一个机制称为乐观锁.
Redis提供了watch机制,它是一种乐观锁

watch命令描述:WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)

在 Redis 中使用 watch 命令可以决定事务是执行还是回滚

一般而言,可以在 multi 命令之前使用 watch 命令监控某些键值对,然后使用 multi 命令开启事务,执行各类对数据结构进行操作的命令,这个时候这些命令就会进入队列。

当 Redis 使用 exec 命令执行事务的时候,它首先会去比对被 watch 命令所监控的键值对,

如果没有发生变化,那么它会执行事务队列中的命令,提交事务;
如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。
无论事务是否回滚 , Redis 都会去取消执行事务前的 watch 命令

Redis watch流程图:

Redis 在执行事务的过程中 , 并不会阻塞其他连接的并发,而只是通过 比较 watch 监控的键值对去保证数据的一致性 , 所 以 Redis 多个事务完全可 以在非阻塞的多线程环境中井发执行,而且 Redis 的机制是不会产生 ABA 问题的, 这样就有利于在保证数据一致的基础上 , 提高高并发系统的数据读/写性能。

使用watch成功提交的事务的案例

127.0.0.1:6379> SET key1 value1
OK
127.0.0.1:6379> WATCH key1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key2 value2
QUEUED
127.0.0.1:6379> EXEC
1) OK
127.0.0.1:6379> 

这里我们使用了 watch 命令设置了 一个 key1 的监控 , 然后开启事务设置 key2 , 直至exec 命令去执行事务.
如果在当前会话中修改key1的值,也是可以成功的。

使用watch回滚的事务的案例

客户端一:

127.0.0.1:6379> SET key1 value1
OK
127.0.0.1:6379> WATCH key1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key2 value2
QUEUED# 在这一步暂停下,打开第二个客户端去修改key1的值,然后再exec
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>

客户端二:

然后回到客户端1 执行exec:

注意 T2 和 T6 时刻命令的说明,数据已经被回滚了,并没有执行事务。

1.3 watch注意事项

Redis禁止在multi和exec之间执行watch指令,而必须在multi之前盯住关键变量,不然会出错。

2、pubsub(消息的订阅和发布)

这是redis一种新型的数据结构,不再依赖那5种基本数据结构了。

pubsub的缺点:

pubsub的消息不能持久化,宕机消息会丢失。pubsub没有消息的Ack通知机制,我们不知道消息消费没有。

正是由于这些缺点,在消息队列领域几乎找不到它的使用场景。

2018年6月,Redis5.0新增了Stream类型的数据结构,给Redis带来了持久化的消息队列,从此pubsub功能可以休息了。

3、Redis的内存回收机制

前置问题:

Q1、设置的 key 明明已经过期了,为啥 仍然占用内存?
Q2、设置的 key 明明还没有过期,为啥 这个 key 就不见了?

还有两个更神奇的问题:

Q3、这个key根本没有设置expire过期时间,为啥不见了?
Q4、我已经大批量删除了这个前缀的key,为啥内存没有啥变化?
这4个问题,通过 Redis 内存回收机制能够得到完美的解答。

先来解释Q4的问题答案:
Redis并不总是将空闲内存立即归还给操作系统。
如果当前的Redis内存有10G,当你del删除了1GB的key后,再去观察内存,你会发现内存变化不大。原因是Linux操作系统是以页为单位来回收内存的,这个页上只要还有一个key在使用,那么它就不能回收。 redis虽然删除了1G的key,但是这些key分散到很多页中,每个页面还有其它key存在,这就导致了内存不会被立即回收。

Redis虽然无法保证立即回收已经删除的key的内存,但是他会重新使用那些尚未回收的内存。就好比电影院,观众走了,位置还在,下一波观众来了直接坐上。而操作系统回收内存就好比把电影院座位也给搬走了。对redis来说,需要重复开辟内存空间。

下面说一下内存回收的机制:
Redis 在两种情况下会回收 key 占用的内存:
1、用户主动设置过期时间的key,时间到了,被回收
2、redis 中key达到了 redis 设置的 max_memory ,内存溢出。
Redis 通过 LRU 算法进行Redis 内存回收。

3.1、删除达到过期时间的键对象

在 Redis 进程内保存了大量用户存入的 key ,针对设置了过期时间的 key ,
如果每一个 key 进行精准的控制-当key过期立即回收空间,对于单线程的Reids来说成本太高。
所以 Redis 中的 key 过期了,占用的内存空间并不会马上被回收,
Redis 采用了两种方式:惰性删除和定时任务删除 来进行空间回收

定时任务删除

Redis 内部维护一个定时任务,每秒执行10次。定时随机的进行过期key的内存回收。
如每次随机回收100key。

惰性删除

当客户端进行某个 key 的get 访问,该key被设置了过期时间,如果此时 get 操作的时候 key 过期了,此时 Redis 将会针对该 key 占用的空间进行回收。

优点:该方式采用用户访问的方式进行空间回收,无需维护 key 的 TTL 链表数据。缺点:如果存在大量已过期的 key 但是长时间内用户一直没有进行 get 方法,会导致过期 key 堆积在内存中,产生内存泄漏。

3.2、内存使用达到 max memory 上限时触发内存溢出控制策略

Redis 是一个缓存中间件,使用的内存空间,Redis 可以通过配置每个 Redis 实例的内存上限,或者 载体机器的内存上限。

在 Redis 的最大内存达到上限的时候,需要进行内存回收,或者拒绝写等策略来保证 Redis 正常提供服务。
Redis 支持6种策略:

noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时Redis只响应读操作。volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,删除最少使用的key,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。volatile-random:随机删除过期键,直到腾出足够空间为止。volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。allkeys-lru:根据LRU算法删除键,删除最少使用的key,不管数据有没有设置超时属性,直到腾出足够空间为止。allkeys-random:随机删除所有键,直到腾出足够空间为止。

4、redis的过期策略

1、如果只是拿redis做缓存,那么使用allkeys-xxx策略,客户端写缓存时不必携带过期时间。2、如果想同时使用redis的持久化功能,那么久使用volatile-xxx策略,这样可以保留没有设置时间的key,它们是永久的key,不会被LRU算法淘汰。

5、LFU VS LRU

LRU是按照“最少使用的原则”,把冷数据从内存淘汰出去。
LFU是Redis4.0版本后,比LRU更优秀。
LFU(Least Frequently Used),表示按“最近的访问频率”进行淘汰,将冷数据清除。

Redis4.0版本后给maxmemory-policy增加了2个选项,分别是volatile-lfu和allkeys-lfu。分别对携带过期时间的key和所有key进行冷数据淘汰。

五、redis的集群和高可用方式

1、CAP原理

CAP原理就好比分布式领域的牛顿定律,它是分布式存储的理论基石。
C:Consistent,一致性
A: Availability,可用性
P:Partition tolerance,分区容忍性

六、redis5.0新特性

1、Stream(有Ack机制的消息队列)

Stream借鉴了kafka的设计。消息时持久化的,重启后消息仍然还在。

redis核心原理与设计思想相关推荐

  1. 新书介绍 -- 《Redis核心原理与实践》

    大家好,今天给大家介绍一下我的新书 -- <Redis核心原理与实践>. 后端开发的同学应该对Redis都不陌生,Redis由于性能极高.功能强大,已成为业界非常流行的内存数据库. < ...

  2. 自顶向下语法分析原理与设计思想

    这里重在讲解设计思想和原理,由于时间久远,一些知识的记忆不太清晰,若有不当之处,请多多指出. 编译原理中的自顶向下语法分析的算法步骤有点麻烦,但是原理与设计思想,及其简单,甚至于没有技术含量. 首先解 ...

  3. Redis核心原理与应用实践

    Redis核心原理与应用实践 在很多场景下都会使用Redis,但是到了深层次的时候就了解的不是那么深刻,以至于在面试的时候经常会遇到卡壳的现象,学习知识要做到系统和深入,不要把Redis想象的过于复杂 ...

  4. 冯洛伊曼计算机工作原理的设计思想是,冯诺依曼计算机工作原理的设计思想是...

    冯诺依曼计算机工作原理的设计思想是 5.下列字符中,ASCII码值最大的是 ( ). A.w B.Z C.A D.9 [答案]A [解析]大家应该记住常用字符的ASCII码值,这样也比较方便平时的学习 ...

  5. 冯诺依曼计算机工作原理的设计思想是,冯诺依曼计算机工作原理的设计思想是.doc...

    1.冯·诺依曼计算机工作原理的设计思想是 (???? ). A.程序设计 B.程序存储 C.程序编制 D.算法设计 [][] 语言是用来编程计算机程序的工具,有高级语言(如VB.VFP)和低级语言(汇 ...

  6. 深入理解消息队列(场景,对比,原理和设计思想)

    导语 | 消息队列也通常称为消息中间件,提到消息队列,大部分互联网人或多或少都听过该名词.对于后端工程师而言,更是日常开发中必备的一项技能.随着大数据时代的到来,apache旗下的Kafka一度成为消 ...

  7. 【个人整理】一文看尽目标检测算法SSD的核心架构与设计思想

    前言:SSD(Single Shot MultiBox Detector)是大神Wei Liu在 ECCV 2016上发表的一种的目标检测算法.对于输入图像大小300x300的版本在VOC2007数据 ...

  8. MapReduce原理与设计思想

    转自:http://www.cnblogs.com/archimedes/p/mapreduce-principle.html 简单解释 MapReduce 算法 一个有趣的例子 你想数出一摞牌中有多 ...

  9. MapReduce原理与设计思想(转载:http://blog.jobbole.com/80619/)

    简单解释 MapReduce 算法 一个有趣的例子 你想数出一摞牌中有多少张黑桃.直观方式是一张一张检查并且数出有多少张是黑桃? MapReduce方法则是: 给在座的所有玩家中分配这摞牌 让每个玩家 ...

最新文章

  1. C语言文件读写(4)-判断文件是否结束
  2. 【机器学习】这次终于彻底理解了奇异值分解(SVD)原理及应用
  3. 从HelloWorld看iphone程序的生命周期
  4. javascript --- vue中简单的模板渲染
  5. 关于数据仓库 — 总体工具介绍
  6. html中使用静态图片做一个csdn网站的首页
  7. 讲的真透彻!还有人不知道什么是AndroidX的吗?已拿offer入职
  8. python xlrd导入后怎么保存_Python xlrd模块导入过程及常用操作
  9. html 弹窗 支持ie8,浏览器兼容性的问题、支持IE8、不支持IE6、想解决这个问题、两个都支持、...
  10. SLF4J介绍以及与LOG4J、LOG4J2、LOGBACK整合方法
  11. 【DVRP】基于matlab蚁群算法求解带距离的VRP问题【含Matlab源码 1040期】
  12. 【SPOJ - NSUBSTR】Substring(统计长度为x的子串最多出现的次数--后缀自动机)
  13. 软件体系结构风格介绍
  14. 计算机专业就业发展现状,计算机专业就业形势分析
  15. 为什么样本方差里面要除以(n-1)而不是n?
  16. Icon Font的转换
  17. The ES9038Q2M SABRE DAC
  18. 段码液晶屏笔段电压范围_一种笔段式液晶屏测试装置的制作方法
  19. 秒表计时器怎么读_手机秒表怎么看读数(秒表的认识与时间的测量)
  20. GBASE数据库启动连接报908 错误

热门文章

  1. 云呐|固定资产如何管理比较好?公司固定资产怎么管理?
  2. 关于无状态服务(stateless service) 有状态服务(stateful service),指一篇文章就搞明白
  3. 再读 ucosII源码(邵贝贝):任务管理
  4. python打开交互界面设计_老司机必备——用PyQt做一个有交互界面的妹子图爬虫...
  5. c#:字符串转int问题
  6. mysql的driverclass_数据库连接driverClass和jdbcUrl大全
  7. 一文讲懂Joinpoint回归原理和Joinpoint官方软件的使用——流病学sci发文利器
  8. vs2017/2019 qt5 联合开发配置
  9. 信贷获客----大数据精准定位获客渠道
  10. 汽车雨刮故障维修要点