特点/为什么这么快

  • 单线程模型:避免了不必要的上下文切面和竞争。
  • 基于内存。
  • 使用I/O多路复用模型,非阻塞I/O。
  • 高效的数据结构(底层实现):动态字符串、双向链表、压缩列表、跳跃表、hash表、整数数组。
  • 根据实际数据类型选择合理的数据编码。

  • 纯内存操作,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。
  • 整个redis就是一个全局哈希表,他的时间复杂度是O(1),而且为了防止哈希冲突导致链表过长,redis会执行rehash操作,扩充哈希桶数量,减少哈希冲突。并防止一次性重新映射数据过大导致线程阻塞,采用渐进式rehash。巧妙的将一次性拷贝分摊到多次请求过程后总,避免阻塞。
  • redis使用的是非阻塞IO:IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,redis采用自己实现的事件分离器,效率比较高。
  • 采用单线程模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
  • redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,调表,使用有序的数据结构加快读取的速度。
  • 根据实际存储的数据类型选择不同的编码。

五种数据类型及应用场景

  1. String字符串(value可为整形、浮点、字符串统称为元素)
    一般做复杂的计数的缓存。
    a. 常用命令get、set、del。
    b. 常用命令自加incr、自减decr、加incrby、减decrby。
  2. list列表,链表实现(实现队列,可重复,FIFO,允许两端推入或弹出元素)
    可做简单的消息队列/利用rlange做基于redis的分页,发布订阅
    a. 常用命令lpush key-name value…、lpop、rpush、rpop、llen(查看长度)、lrange key-name start end。
  3. hash(value存放的是结构化的对象,散列值,key必须唯一)
    对象数据的存储。比较方便的就是操作其中的某个字段,可存用户信息,以cookie ld为key,30分钟为过期时间,能够很好的模拟出类似session的效果.
    a. 常用命令hset添加、hget获取、hdel、hmget获取一个多个数据hmset、hkeys、hvals、hgetall获取所有键值对。
  4. set集合(不重复)
    做全局去重,为什么不用jvm自带的set去重,因系统一般集群.另,利用交集,并集,差集等可计算共同喜好,全部,独有喜好等.
    a. 常用命令sadd添加、scard查看元素个数、sismember判断是否存在某元素、srem删除某元素。
    b. sdiff差集、dinter交集、sunion并集。
  5. zset(sorted set有序列表)
    多了一个权重参数score,集合元素能够按照score进行排序.可做排行榜,取Top N操作。排行榜、弹幕消息
    a. 常用命令zadd添加、zcard查询、zrange数据排序。

数据库--Redis - 图1
数据库--Redis - 图2

Redis与Memcache

  • 数据类型上:memchache只有KV,redis不仅支持简单K/V,还提供list、String、hash、set、zset(sorted set)等数据结构存储。
  • 存储方式上:memchache存内存;redis支持数据的持久化,可将内存中的数据保持在磁盘中,重启的时候再次加载进行使用。
  • Value大小:redis可1G;mem只有1M。
  • redis支持订阅发布、Lua脚本、事务等。
  • Memcached没有原生的集群。
  • Memcached过期数据的删除策略只用了惰性删除。redis6种。

    基本命令

    数据库中存储的数据中,key全是string,value可为String,Set,List,Sorted-set,Hash

    string类型的数据
  • set admin 123

  • get admin
  • del admin
  • exists admin—判断是否存在key为admin,不存在返回0
  • append admin 456—把key为admin的数据追加456
  • strlen admin—获取value的长度
  • incr num—num值增一,incrby num 2—-增加步长为2
  • decr num—-num减一
  • decrby num 2—每次执行减2
  • set admin tom ex 5—-添加数据,使其存活时间5秒
  • set admin tom px 8000—添加数据,存活8000毫秒
  • ttl admin—-看数据能活多久
  • getset admin 123—-获取取当前admin的值,修改当前值
  • set admin “hello world”—-如果value中包含特殊字符,可用”xxx”
  • setrange admin 5 123—-把下标为5处的三个字符替换成123
  • getrange admin 2 4—获取下标为2到4处的字符
  • mset a1 11 a2 22 a3 33—-可一次添加多个键值对,这里是三对
  • mget a1 a2 a3 admin—-批量获取数据

    list类型的数据

    是一种顺序存储的双向链表,在头和尾增删元素效率高,中间效率低,应用场景:最新消息排行、消息队列。

  • lpush list a b c d—-添加4个元素,下标为0的元素d,每次都添加到头部(左边)。

  • rpush lly a b c d添加顺序跟上面相反,每次都添加到尾部(右部)
  • lrange list 0 2—-获取元素为d c b
  • lrange list 0 -1—-获取list中全部元素
  • lpop list—-弹出栈顶元素并删除元素
  • llen list—-获取list集合元素个数
  • lrem list 2 a—删除元素,如果有 很多个a元素,则删除最多2个,从下标为0位置
  • lindex list 2—获取下标为2的元素
  • lset list 2 hello—-修改下标为2位置处元素的值hello
  • del list—-删除key为list的元素

    set类型

    元素不重复,应用场景:利用唯一性,统计访问网站的所有ip

  • sadd set a a b——向set集合中只放入2个元素

  • smembers set—-查看set集合中元素
  • scard set—-获取集合中元素数量
  • sismember set a——判断a元素在set中是否存在
  • srandmember set—-随机获取一个元素
  • srandmember set 2—随机获取两个
  • spop set 1—弹出集合中一个元素,从集合中删除
  • srem set d—-删除指定的元素d
  • del set —-删除set中全部元素

    sorted-set

    每个元素都有一个权重,元素自动按权重排序,元素不能重复,但权重可重复,应用场景:排行榜。

  • zadd lly 10 a 8 b 15 c—-添加三个元素a,b,c,其权重分别为10,8,15

  • zrange lly 0 -1—-显示全部元素
  • zrange lly 0 -1 withscores—-带权重查看元素
  • zcard lly—有几个元素
  • zrank lly b—-获取指定元素的下标
  • zrem lly b—删除指定元素
  • zscore lly c—-显示指定元素的权重
  • zrevrange lly 0 -1—-倒序显示元素

    hash类型

    可理解为java中的Map,应用场景:购物车

  • hset lly user tom—-添加一对键值对

  • hmset lly phone 1391222 address nj
  • hget lly user
  • hmget lly user phone age
  • hgetall lly—-获取全部元素
  • hkeys lly—-获取全部key
  • hvals lly—-获取全部value集合
  • hlen lly—获取多少个键值对
  • hdel lly phone
  • hexists lly user—-判断是否存在key为user的元素,有则返回1没有返回0

    常规操作
  • flushdb—清空当前选择的数据库,flushdball清空所有数据库中数据

  • keys * —-全部key
  • keys a* —-a开头的key
  • select 数字—-选择某一个数据库,一共16个,第一个为0,最后一个为15

持久化机制

RDB(快照)

RDB(redis Database):默认方式,用数据集快照的方式,半持久化模式记录redis数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

  • 优点:只有一个dump.rdb文件,方便持久化.容灾性好,一个文件可保存到安全的磁盘.性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化.使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能.相对于数据集大时,比AOF的启动效率高.
  • 缺点:数据安全性低,RDB是时间间隔持久化,如果持久化之间发生故障,会发生数据丢失,更适合数据要求不严谨的时候用。
  • 自动保存

    1. save 900 1 --15分钟修改就会保存,dump.rdb文件
    2. save 300 10 --5分钟内修改10次自动保存
    3. save 60 10000 --1分钟修改1万次保存

    AOF(追加命令)

    AOF(Append-only file)持久化方式:所有的命令记录以redis命令请求协议的格式完全持久化存储保存为aof文件。

  • 优点:数据安全,aof可配置appendfsync属性,有always,没进行一次命令操作就记录到aof文件中一次。通过append模式写文件,即使中途宕机,可通过redis-check-aof工具解决数据一致性问题。aof机制的rewrite模式。aof文件没被rewrite之前(文件过大时会对命令进行合并重写),可删除其中的某些命令(比如误操作的flushall)。

  • 缺点:aof文件比rdb文件大,且恢复速度慢。
  • 数据集大的时候,比rdb启动效率低。
  • 所有写指令记录到缓冲区满了就一次性写入到appendonly.aof中,在服务器重启是,会首先读取appendonly.aof文件。前提是修改配置appendonly no/yes
  • appendonly.aof可与dump.rdb共存,有限读取aof文件

    双写一致性

  • (Cache Aside Pattern)旁路缓存模式:

    • 读的时候:先读缓存,没有读数据库,然后写入缓存,再返回响应。
    • 更新的时候:先更新DB,然后删除cache。
      - 初级方法:先删缓存,再更新数据库(防止删除缓存失败出现不一致)
  • 延时双删:先删除缓存,再写数据库,休眠xx毫秒,最后再次删除缓存。

    Redis并发竞争key解决方案

  • 分布式锁+时间戳

  • 利用消息队列

    事务

    一组命令的集合。

  • 总结一句话:一次性、顺序性、排他性的执行一个队列中的一系列命令。

  • redis事务三阶段

    开始事务、命令入队、执行事务。

  • redis事务没有隔离级别的概念

    批量操作在发送exec命令前被放入队列缓存,并不会被实际执行,也就不要存在事务内的查询要看到事务里的更新,事务外查询不能看到。

  • 不保证原子性redis中,单条命令时原子执行的,但事务不保证原子性且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。若有不能入队,事务直接结束,若有一条失败,其他语句照样执行,也就是说redis单条语句是原子的,但事务不支持原子性。

  • 相关命令

    • watch key1 key2…:监视key,若在事务主席那个前,被监视的key被其他命令改动则事务被打断(类似乐观锁)。
    • multi:开启事务。
    • exec:执行所有事务块的命令(一旦执行exec后,之前加的监控锁都会被取消掉)。
    • discard:取消事务,放弃事务块中的命令。
    • ubwatch:取消watch对所有key的监控。

      watch操作

  • 类似乐观锁,只检测版本号,每次改动后会把版本号加一,若本次提交时发现版本号比当前事务中保存的大,提交失败。

    客户端

  • Jedis

  • Lecture
  • Redisson

    缓存淘汰机制

  • noeviction:不会驱逐任何key,不删除,超内存返回错误。

  • allkeys-lru:所有,优先删除最少使用的。
  • volatile-lru:只限于设置了expire部分,优先删最近最少使用的。
  • allkeys-random:对所有key随机删除
  • volatile-random:只限于设置了expire部分,随机。
  • volatile-ttl:只限于设置了expire部分,优先删除块过期的。
  • allkeys-lfu:对所有key使用LFU算法进行删除
  • volatle-lfu:对所有设置了过期时间的key使用LFU算法进行删除.
  • 总结:一般生产中使用allkeys-lru

    LRU具体实现

  • 传统的LRU是使用栈的形式,每次都将最新使用的移入栈顶,但是用栈的形式会导致执行select *的时候大量非热点数据占领头部数据,所以需要改进。Redis每次按key获取一个值的时候,都会更新value中的lru字段为当前秒级别的时间戳。Redis初始的实现算法很简单,随机从dict中取出五个key,淘汰一个lru字段值最小的。在3.0的时候,又改进了一版算法,首先第一次随机选取的key都会放入一个pool中(pool的大小为16),pool中的key是按lru大小顺序排列的。接下来每次随机选取的keylru值必须小于pool中最小的lru才会继续放入,直到将pool放满。放满之后,每次如果有新的key需要放入,需要将pool中lru最大的一个key取出。淘汰的时候,直接从pool中选取一个lru最小的值然后将其淘汰。

    过期删除

  • 定时,过期时间

  • 惰性删除:取key的时候对数据进行过期检查。
  • 定期删除:每隔一段时间抽一批过期key。

    主从

  • 将读写分离,通过配置多个从服务器,执行读,主服务器可读可写,从而实现负载均衡。

    • 手动主配置,slaveof no one
    • 手动从配置, slaveof ip port(没有冒号)
    • redis-server redis.conf(指定配置文件启动,可模拟实现单机主从)
  • 哨兵

    • 主从中自动监视各服务器,主卦投票从升级主,主恢若复则为从。

      订阅发布

      集群

      穿透、击穿、雪崩

      穿透

  • 用户使用一个不存在的key从redis中查询—>redis失败—>传统数据库—>查询失败。这一过程反复出现则可能压垮数据库。

  • 解决:
    • 首先校验参数是否合法,返回空/异常。
    • 缓存无效key,设置较短的过期时间。
    • 布隆过滤(误判:存在不一定,不存在就一定不存在)

数据库--Redis - 图3

雪崩

  • 缓存一段时间内都失效,导致后面请求都落在数据库上,直接崩。
  • 解决:

    • 高可用架构,集群。
    • 对每个数据设置随机过期时间,避免集体失效。
    • 通过队列控制并发量、限流降级。

      击穿

  • 对于一些设置了过期时间的key,当这个key在失效的时候,被高并发访问,持续的高并发就突破缓存,直接请求数据库,从而可能瞬间把后端db压垮。

  • 解决:1设置i请求队列或添加互斥锁。2、设置该数据永远不过期。

  • Redis不适用的事:
    • 当数据量太大、数据访问频率非常低的业务都不适合使用Redis,数据太大会增加成本,访问频率太低,保存在内存中纯属浪费资源。
  • redis hash字典
    • redis整体就是一个哈希表来保存所有的键值对,无论数据类型是5种的任意种。哈希表本质就是一个数组,每个元素被叫做哈希桶,不管什么数据类型,每个桶里面的entry保存着实际具体值的指针。
    • 整个数据库就是一个全局哈希表,哈希表的时间复杂的是O(1),只需要计算key的哈希值,就知道对应的哈希桶位置,定位桶里面的entry找到对应数据,整个也是Redis块的原因之一。
  • 哈希冲突
    • redis通过链式哈希解决冲突:同一个桶里的元素用元素链表保存。但是当链表过长就会导致查找性能变差,所以redis为了追求快,使用了两个全局哈希表。用于rehash操作,增加现有的哈希桶数量,减少哈希冲突。开始默认使用hash表1保存键值对数据,哈希表2此刻没有分配空间,当数据越来越多触发rehash操作,执行:
      • 1、给hash表2分配更大的空间。
      • 2、将hash表1的数据重新映射拷贝到hash2表中。
      • 3、释放hash表1的空间。

将hash表1的数据重新映射到hash表2的过程并不是一次性的。这样会造成redis阻塞,无法提供服务。而是采用渐进式rehash,每次处理客户端请求的时候,先从hash表1中第一个索引开始,将这个位置的所有数据拷贝到hash表2中,就这样将rehash分散到多次请求过程中,避免耗时阻塞。

  • zipList压缩列表
    • 压缩列表是List、hash、sortedSet三种数据类型底层实现之一。
    • 当一个列表只有少量数据的时候,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。
    • ziplist是一系列特殊编码的连续内存块组成的顺序型的数据结构,可包含多个entry节点,每个节点可存放整数或字符串。
  • 双端列表
    • redis list数据类型通常被用于队列、微博关注人时间轴列表等场景。不管是先进先出队列还是先进后出队列,双端列表都很好的支持这些特性。
    • 后续版本对列表数据结构进行了改造,使用quicklist代替了ziplist和linkedlist。
    • quicklist是ziplist和linkenlist的混合体,他将linkedlist按段切分,每一段使用ziplist来紧凑存储,多个ziplist之间使用双向指针串接起来。
  • 跳跃表skiplist
    • sorted set类型的排序功能是通过[跳跃列表]数据结构实现的。平均O(logN)最坏O(N)
  • 整数数组intlist
    • 当集合只包含整数,且数量不多,redis会使用整数集合作为集合键的底层实现。

  • 单线程模型
    • 需要明确的是:redis的单线程指redis的网络IO及键值对指令读写是由一个线程来执行的。对于持久化、集群数据同步、异步删除等都是其他线程执行的。
    • 多线程的弊端:多线程可增加系统吞吐量,充分利用CPU资源。但是,没有良好的设计,会出现增加了线程数量,前期吞吐量增加,进一步新增线程,吞吐量几乎不再新增,甚至会下降。
    • 切换CPU上下文,非常消耗资源。
    • 多线程修改共享数据,为了保证数据正确,需要加锁带来额外开销,面临的共享资源的并发访问控制问题。
    • 引入多线程,需要使用同步原语来保护共享资源的并发读写,增加代码复杂度和调试难度。
  • 单线程是否没有充分利用cpu资源
    • 官方答案:因redis是基于内存的操作,cpu不是redis的瓶颈,redis的瓶颈最有可能是机器内存的大小或者网络宽带。
  • IO多路复用模型
    • redis采用IO多路复用,并发处理连接。采用epoll+自己实现的见到那的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在IO上浪费时间。
  • 基本IO模型
    • 和客户建立accept;
    • 从socket中读取请求recv;
    • 解析客户端发送的请求parse;
    • 执行get指令;
    • 响应客户端数据,向socket写回数据。
    • 阻塞IO

数据库--Redis - 图4

  • IO多路复用
    • 多路复用指的是多个socket连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。他的基本原理是,内核不是监视应用程序本身的连接,而是监视应用程序的文件描述符。

      案例

  1. 显示最新回复
    1. select * from tname where ... order by creat_time desc limit 5;

    回复是按照时间创建的,现在要取与存入顺序相逆的5个,耗时。可用redis实现优化。 每次回复将id加入redis列表

  1. LPUSH latest.comments <ID>;
  2. //保存最新的5000条记录的ID,超过的数据去数据库查询
  3. LTRIM latest.comments 0 5000;
  4. //获取最新评论的函数
  5. List<String> getLatetComments(start,numItems){
  6. idList = redis.lrange("latest.comments",start,start+numItem - 1)
  7. if(idList.length < numItems){
  8. idList = XXXDAO.query("select ... order by create_time limit ...")}
  9. return idList;
  10. }
  1. 排行榜相关

    大部分数据都是再硬盘数据库存的,有些游戏根据实时分数进行前几名或全球排行的几乎每秒都需要读取,传统数据库不太理想。对redis来说几百万小菜一碟。

  1. zadd leaderboard <score> <username>
  2. //得到前100名高分用户
  3. zrevarange leaderboard 0 99
  4. //全球排名
  5. zrank leaderboard <username>
  1. 消息队列

    除了list push pop 等,还有list pop的变体命令,能够在列表为空时阻塞队列。 现在互联网应用大量使用了消息队列(Messaging)。消息队列不仅被用于系统内部组件间的通信,同时也被用于系统跟其他服务之间的交互。消息队列的使用可增加系统的可扩展性、灵活性和用户体验。非基于消息队列的系统,其运行速度取决于系统中最慢的的速度(短板效应)。基于消息队列可将系统各组件接触耦合,这样系统就不再受最慢组件的束缚,各组件可异步运行更快的完成各自工作。 Pub/Sub发布订阅

  2. 缓存

    能够代替memcached,让缓存只能存储变为能够更新,因此不需要每次都重新生成数据了。

  3. 关系

    利用集合的一些命令,比如求交集、并集、差集等。可以方便得到一些共同好友、共同爱好之类的功能;

  4. 计算器/限速器

    利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;

  5. Session共享


参考1
参考2