Redis简介
关系型数据库
关系型数据天然就是二维的、表格式的,因此存储在数据表的行和列中,支持ACID4大事务特性,数据模型可扩展性差、全文检索功能较弱
NoSQL数据库
Not Only SQL简称NoSQL,有灵活的数据模型,易扩展。有非常高的读写性能。无须事先为要存储的数据建立字段,随时可以存储自定义的数据格式
NoSQL的应用场景
1、高并发数据读写
2、数据接口易扩展
3、存储大量数据
4、速度快
不适用场景:
1、对事务要求较高的场景
2、不适合结构化的sql查询方案
Redis的定义
redis是一个key-value的内存存储数据结构服务器,一般用作高速缓存、分布式锁、临时数据库等等
Memcache、Redis、MongoDB、HBase区别介绍
Memcache
1、支持简单的key-value模式存储,支持的数据结构很单一,仅支持string类型的操作
2、数据存储在内存中,不支持数据的持久化
Redis
1、除了key-value外,还支持丰富的存储接口如string、set、zset、hash、list、hyperLogLog等
2、数据存储在内存中,支持AOF、RDB等持久化方式(支持备份)
3、一般做缓存数据库
4、覆盖Memcache的功能
MongoDB
1、文档型数据库
2、数据都存在内存中、内存不足时把不常用的数据保存到硬盘
3、虽然是Key-value,但是对value(尤其是json)提供丰富的查询功能(比如sql)
4、支持二进制数据及大型对象
HBase
1、适用于大数据环境
2、基于Hadoop,数据最终存储在HDFS
3、支持存储非常庞大的表,单表支持10亿行数据,并且支持上百万个列
Redis VS MongDB
Redis主要把数据存储在内存中,其“缓存”的性质远大于其“数据存储“的性质,其中数据的增删改查也只是像变量操作一样简单;
MongoDB却是一个“存储数据”的系统,增删改查可以添加很多条件,就像SQL数据库一样灵活,这一点在面试的时候很受用。
Redis安装与使用
1 | |
1 | |
StringRedisTemplate与RedisTemplate
spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。在 RedisTemplate中提供了几个常用的接口方法的使用,分别是:

StringRedisTemplate继承自RedisTemplate,也一样拥有上面这些操作。 StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存 的。RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
Redis客户端命令对应的RedisTemplate中的方法列表:
| String类型结构 | |
|---|---|
| Redis | RedisTemplate rt |
| set key value | rt.opsForValue().set(“key”,”value”) |
| get key | rt.opsForValue().get(“key”) |
| del key | rt.delete(“key”) |
| strlen key | rt.opsForValue().size(“key”) |
| getset key value | rt.opsForValue().getAndSet(“key”,”value”) |
| getrange key start end | rt.opsForValue().get(“key”,start,end) |
| append key value | rt.opsForValue().append(“key”,”value”) |
| Hash结构 | |
| hmset key field1 value1 field2 value2… | rt.opsForHash().putAll(“key”,map) //map是一个集合对象 |
| hset key field value | rt.opsForHash().put(“key”,”field”,”value”) |
| hexists key field | rt.opsForHash().hasKey(“key”,”field”) |
| hgetall key | rt.opsForHash().entries(“key”) //返回Map对象 |
| hvals key | rt.opsForHash().values(“key”) //返回List对象 |
| hkeys key | rt.opsForHash().keys(“key”) //返回List对象 |
| hmget key field1 field2… | rt.opsForHash().multiGet(“key”,keyList) |
| hsetnx key field value | rt.opsForHash().putIfAbsent(“key”,”field”,”value” |
| hdel key field1 field2 | rt.opsForHash().delete(“key”,”field1”,”field2”) |
| hget key field | rt.opsForHash().get(“key”,”field”) |
| List结构 | |
| lpush list node1 node2 node3… | rt.opsForList().leftPush(“list”,”node”) |
| rt.opsForList().leftPushAll(“list”,list) //list是集合对象 | |
| rpush list node1 node2 node3… | rt.opsForList().rightPush(“list”,”node”) |
| rt.opsForList().rightPushAll(“list”,list) //list是集合对象 | |
| lindex key index | rt.opsForList().index(“list”, index) |
| llen key | rt.opsForList().size(“key”) |
| lpop key | rt.opsForList().leftPop(“key”) |
| rpop key | rt.opsForList().rightPop(“key”) |
| lpushx list node | rt.opsForList().leftPushIfPresent(“list”,”node”) |
| rpushx list node | rt.opsForList().rightPushIfPresent(“list”,”node”) |
| lrange list start end | rt.opsForList().range(“list”,start,end) |
| lrem list count value | rt.opsForList().remove(“list”,count,”value”) |
| lset key index value | rt.opsForList().set(“list”,index,”value”) |
| Set结构 | |
| sadd key member1 member2… | rt.boundSetOps(“key”).add(“member1”,”member2”,…) |
| rt.opsForSet().add(“key”, set) //set是一个集合对象 | |
| scard key | rt.opsForSet().size(“key”) |
| sidff key1 key2 | rt.opsForSet().difference(“key1”,”key2”) //返回一个集合对象 |
| sinter key1 key2 | rt.opsForSet().intersect(“key1”,”key2”)//同上 |
| sunion key1 key2 | rt.opsForSet().union(“key1”,”key2”)//同上 |
| sdiffstore des key1 key2 | rt.opsForSet().differenceAndStore(“key1”,”key2”,”des”) |
| sinter des key1 key2 | rt.opsForSet().intersectAndStore(“key1”,”key2”,”des”) |
| sunionstore des key1 key2 | rt.opsForSet().unionAndStore(“key1”,”key2”,”des”) |
| sismember key member | rt.opsForSet().isMember(“key”,”member”) |
| smembers key | rt.opsForSet().members(“key”) |
| spop key | rt.opsForSet().pop(“key”) |
| srandmember key count | rt.opsForSet().randomMember(“key”,count) |
| srem key member1 member2… | rt.opsForSet().remove(“key”,”member1”,”member2”,…) |
Lua脚本
代码示例上面已经给出:
管道(Pipeline)
客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一 次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。 pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信 息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败,后面命令 不会有影响,继续执行。
特定
1、减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器 上完成。使用脚本,减少了网络往返时延。这点跟管道类似
2、原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过 redis的批量操作命令(类似mset)是原子的
3、替代redis的事务功能:redis自带的事务功能很鸡肋,报错不支持回滚,而redis的lua脚本几乎实现了 常规的事务功能,支持报错回滚操作,官方推荐如果要使用redis的事务功能可以用redis lua替代
注意,不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令, 所以使用 时要注意不能出现死循环、耗时的运算。redis是单进程、单线程执行脚本。管道不会阻塞redis。
Redis核心数据结构与原理
Redis核心数据结构与应用
Redis核心数据结构
5种核心数据结构

String
字符串常用操作
1 | |
原子加减
1 | |
String应用场景
1 | |

Hash
Hash常用操作
1 | |
Hash应用场景
1 | |
1 | |
优缺点
优点
1)同类数据归类整合储存,方便数据管理
2)相比string操作消耗内存与cpu更小
3)相比string储存更节省空间
缺点
1)过期功能不能使用在field上,只能用在key上
2)Redis集群架构下不适合大规模使用(由于集群模式根据key来确定存储在那个节点,如果某个hset存储内容过多,会造成某个节点的存储量偏大,造成数据分布不均匀)

List结构
List常用操作
1 | |
List应用场景
1 | |
诸葛微博消息和微信公号消息
微博关注了MacTalk,备胎说车等大V
1 | |

Set结构
Set常用操作
1 | |
Set应用场景
微信抽奖小程序
1 | |

微信微博点赞,收藏,标签
1 | |
集合操作
1 | |

集合操作实现微博微信关注模型
1 | |

集合操作实现电商商品筛选
1 | |

Zset结构
ZSet常用操作
1 | |
Zset应用场景
1 | |

HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法
基数计算(cardinality counting)指的是统计一批数据中的不重复元素的个数,常见于计算独立用户数(UV)、维度的独立取值数等等
Redis更多应用场景
微博、微信、陌陌<附近的人>
微信<摇一摇><抢红包>
滴滴打车、摩拜单车<附近的车>
美团和饿了么<附近的餐馆>
搜索自动补全
布隆过滤器
。。。
Redis核心原理
Redis 为什么一开始选择单线程模型
因为Redis是一个基于内存的数据库,还要处理大量的外部的网络请求,这就不可避免的要进行多次IO。好在Redis使用了很多优秀的机制来保证了它的高效率。那么为什么Redis要设计成单线程模式的呢?
1、IO多路复用
Redis在网络I/O模型上采用了多路复用技术,来减少网络瓶颈带来的影响。很多场景中使用单线程模型并不意味着程序不能并发的处理任务。Redis 虽然使用单线程模型处理用户的请求,但是它却使用 I/O 多路复用技术“并行”处理来自客户端的多个连接,同时等待多个连接发送的请求。使用 I/O 多路复用技术能极大地减少系统的开销,系统不再需要为每个连接创建专门的监听线程,避免了由于大量的线程创建带来的巨大性能开销。
2、可维护性
多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题。单线程模式下,可以方便地进行调试和测试。
3、基于内存操作
多线程能够充分利用 CPU 的资源,但对于Redis来说,由于基于内存速度那是相当的高,能达到在一秒内处理10万个用户请求,如果一秒十万还不能满足,那我们就可以使用 Redis 分片的技术来交给不同的 Redis 服务器。这样的做饭避免了在同一个 Redis 服务中引入大量的多线程操作。 而且基于内存,除非是要进行 AOF 备份,否则基本上不会涉及任何的 I/O 操作。这些数据的读写由于只发生在内存中,所以处理速度是非常快的;用多线程模型处理全部的外部请求可能不是一个好的方案
4、线程上下文切换问题
多线程场景下会发生线程上下文切换。线程是由 CPU 调度的,CPU 的一个核在一个时间片内只能同时执行一个线程,在 CPU 由线程A切换到线程B的过程中会发生一系列的操作,主要过程包括保存线程A的执行现场,然后载入线程B的执行现场,这个过程就是“线程上下文切换”。其中涉及线程相关指令的保存和恢复。 频繁的线程上下文切换可能会导致性能急剧下降,这会导致我们不仅没有提升处理请求的速度,反而降低了性能。
Redis6.0之前的版本真的是单线程?
Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大key的删除等等

Redis6.0为什么引入多线程?
参考文档
简单来说,可以充分利用服务器CPU资源,目前主线程只能利用一个核,多线程任务可以分摊Redis同步IO读写负荷。
Redis 将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis 服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。
但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。
常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个 Redis 服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。
虽然现在很多服务器都是多个 CPU 核的,但是对于Redis来说,因为使用了单线程,在一次数据操作的过程中,有大量的 CPU 时间片是耗费在了网络 IO 的同步处理上的,并没有充分的发挥出多核的优势。
如果能采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。
Redis6.0引入的多线程部分,实际上只是用来处理网络数据的读写和协议解析,执行命令仍然是单一工作线程,而且多线程开关默认关闭
Zset的底层原理
底层结构
底层实现 = hash(映射分数和成员) + 压缩列表(zipList) 或者 跳表(skipList)
什么是跳表
跳表在 Redis 的 zset 中主要用于支持范围查询操作,因为它能够在对数时间复杂度内完成查找、插入和删除操作。跳表实际上是一种多层有序链表,每一层链表都是有序的,且层数越高的链表包含的节点越少,这样通过多层链表的辅助,可以快速定位到目标节点。
什么时候用跳表
当 ZSET 中的元素数量较小时,Redis 会选择使用有序数组作为底层实现。这是因为有序数组在内存使用效率上更高,而且对于较小的集合来说,插入、删除和查找操作的时间复杂度都很低。
然而,当 ZSET 中的元素数量较大时,Redis 会自动将底层实现从有序数组切换为跳跃表。跳跃表在处理大型有序集合时更加高效,因为它可以在对数时间内完成插入、删除和查找操作,并且在内存占用上也比较合理。
跳表和红黑树的对比
总的来说,跳表和红黑树的查询复杂度都能达到O(LogN),但是红黑树在增删过程中需要平衡,实现难度比较复杂,而跳表需要维护多个链表,就空间而言需要更多内存,但更为直观。
使用zipList的条件
- 成员数量小于128个: 当成员数量较少时,使用有序数组作为底层实现更为合适。有序数组可以通过紧凑的方式存储成员和分数,节省内存空间,并且在插入、删除和查找操作上有较低的时间复杂度。因此,当成员数量较少时,使用有序数组可以更有效地利用内存和提高性能。
- 每个成员的字符串长度小于64个字节: 在有序数组中,每个成员都需要存储其对应的字符串以及相关的分数。为了保持内存占用的合理性,限制每个成员的字符串长度可以有效控制存储空间的使用。较短的字符串长度意味着每个成员占用的空间更小,从而可以存储更多的成员在有限的内存空间内。
其他高级命令
keys
keys: 全量遍历键,用来列出所有满足特定正则字符串规则的key,当redis数据量比较大时, 性能比较差,要避免使用
1、keys *
2、keys a*
scan
scan:渐进式遍历键:SCAN cursor [MATCH pattern] [COUNT count] scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三个是一次遍 历的key的数量,并不是符合条件的结果数量。第一次遍历时,cursor 值为 0,然后将返回结果中第 一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

Info
Info:查看redis服务运行信息,分为 9 大块,每个块都有非常多的参数,这 9 个块分别是:
Server 服务器运行的环境参数
Clients 客户端相关信息
Memory 服务器运行内存统计数据
Persistence 持久化信息
Stats 通用统计数据
Replication 主从复制相关信息
CPU CPU 使用情况
Cluster 集群信息
KeySpace 键值对统计数量信息