Redis
beforeInter——技术发展
解决功能性问题——完成最基本的CRUD
解决扩展性问题——通过符合某种规范/避免复杂的写法,将对程序的扩展简化,不必回回更改源码(甚至大改)
解决性能的问题——当用户量不断增加,需要解决性能不足问题
技术架构发展
- Web 1.0
-
Web 2.0
由于移动端接入,导致访问量剧增,导致CPU及内存压力,以及数据库IO压力
解决CPU和内存的压力:
分布式集群 + 反向代理|负载均衡
分布式的一个问题是:关键数据要在多个服务器中同步,例如明确客户端身份的Session
做法:用NoSQL存储到内存中
解决IO压力:
常规的均衡数据库IO的操作——分表分库,读写分离
但带来的问题是:会破坏一定的业务逻辑来换取性能
做法:
- 本地缓存,减少IO数据库的次数
- 增加缓存数据库:将热点数据另存入一个库中
-
NoSQL
-
not SQL:表示是非关系型数据库,即没有固定SQL语法和限制,简简单单KV存储;表与表(数据和数据)之间也没有明显关系(via foreign key)
-
not only SQL
-
类型
-
Redis k-v形式
-
Document 用JSON保存内容(每个JSON内格式不限)
-
Graph Neo4j 图类型数据库——用图形刻画节点间关系(适合于:社会关系——好友推荐,公告交通网络,地图及网络拓扑)
-
-

-
查询语法
不同类型语法各异
-
ACID
NoSQL基本无法满足对事务的支持,不像关系型数据库,各个产品底层都支持事务
只能满足BASE:
-
适用场景
- 对数据高并发的读写
- 海量数据的读写
- 对数据高扩展性
-
不适用场景
- 需要事务支持
- 基于SQL的结构化查询存储,处理复杂的关系
-
SQL
Structured结构化:
- 存储结构:数据用一张二维表存储,表中每个字段类型有限,范数限制
- 在此添加信息也要符合之前的结构
区别汇总
-
扩展性
垂直:关系型数据库没有设计多台机器负载均衡。传统中增强扩展性主要是增强单个服务器的性能。(但分表分库分片+中间件可以实现)
水平:通过配置集群负载均衡来提高性能
Redis
Remote Dictionary Server 远程词典服务器
基本属性:
- Redis默认有16个数据库,下标从0开始,初始时默认使用0号数据库
- 使用命令select 来切换数据库,例如 select 8
- Redis统一密码管理——所有库使用同样密码
- dbsize 用于查看当前数据库的key的数量
- flushdb 清空当前数据库
- flushall 通杀全部库
redis对比memcache
-
memcache 支持的数据类型比较单一
-
memcache 不支持持久化操作,只能在内存中存储
-
核心技术不同
memcache 使用多线程+锁的机制,从串行模式升级,提高效率
Redis使用单线程+多路IO复用,效率更高
单线程:代理和数据库的连接是单线程
多路IO复用:表示Redis用单个线程监听多个客户端的连接/IO事件,并在不同事件之间进行切换/处理。
实现机制简述:
- select 早期机制,用一个进程轮询多个文件扫描符
- epoll
- kqueue
Redis 命令行工具
-
redis-cli:自带的命令行客户端(client)
redis-cli [optins] [commonds]
options:
- -h 127.0.0.1 : 指定要连接的redis节点的IP地址 默认也就是本地
- -p 6379 :指定端口 默认也就是6379
- -a 指定redis的访问密码
commonds:直接写操作命令(一般不用)
输入ping
回复pong
用于测试
-
redis-server 启动redis服务器
此处还可以设置配置文件,确保是后台允许
docker设置的自启动将此屏蔽
图形化界面
RDM
Redis 命令
数据结构
key-value类型数据
k一般都是字符串
v则很有学问——八种+
- String 字符串
- Hash 哈希表存储另外数据结构
- List 列表
- Set 集合
- SortedSet 排序集合
- GEO 地理坐标
- BitMap 位图
- HyperLog
数据结构的使用场景:
还有特殊的,如消息队列等
命令行中可以查看帮助
@后加要查看的类型
help [options] 用于查看具体某个指令的用法
通用命令
-
keys pattern 查key
pattern是类似正则的表达式
成也正则,败也正则——模糊的性能开销太大(估计要全匹配一次),而redis单线程(BIO)。
但如果redis是集群模式,有主有从,则可以在从数据中乱搞
据说redis分片是将数据分担到分布式服务器中,每个服务器拥有不同的数据,而每个服务器又有集群,主从服务器之间拥有相同数据
keys * 查看所有key
-
DEL key 删除某个k-v
返回值是删除的行数
unlink key 非阻塞式异步删除——仅将keys从keysspace元数据中删除,真正的删除会在后续异步操作
-
EXISTS 判断k是否存在
返回值 1 表示存在 0 表示不存在
-
EXPIRE 给一个key设置有效期,有效期后key会被删除(默认单位是秒)
这是基于内存的考虑
expire k1 10 将k1设置为10秒
-
TTL 查看一个k的有效期
TTL k 变成-2,则表示该k消失
TTL k 是 -1 ,表示永不删除——如果没有设置EXPIRE,则为默认 -1
-
String类型的命令
String类型是二进制安全的,意味着Redis中的string可以包含任何数据,比如jpg图片,或者序列号的对象...
常规存储中,如果以字符串形式存储可能会受限于特定的字符串编码,在客户端发送到服务器 | 字符串-二进制-字符串过程中,编码错误可能导致二进制数据出现问题
而Redis的做法是:不对二进制数据进行任何解析,来什么存什么,在返回相同数据后由客户端相同的编码方式进行解析
理解:常见IDE的操作:输入“hello \t world \n”,IDE会将其中的转义都转了,这就是二进制不安全,因为服务器对传递的二进制进行了分析,返回分析后的模式。
而二进制安全的意思是,不会对存入的字节做任何处理,你怎么存我怎么返回,如果你用utf-8编码,存一个 中 字,占 3 个字节,用GBK编码,占两个字节。存一个 10 占用 2 个字节,存 100 要占 3 个字节。 Redis就 用了二进制安全的技术,不将数据进行任何转换,以二进制的方式来什么存什么。为此,同一个 Redis 服务的客户端要统一编码格式,不然数据的读和存会有不一致的问题。
Redis出于节省空间考虑,对不同类型的String进行不同处理
or say 本身多种基本数据结构对外都包装成String,但底层处理不同
- 如果value的字符串是int,redis按照二进制编码(范围更大),可自增自减
- 如果value的字符串是flout,redis按照float编码,可自增自减
- 如果value的字符串是普通字符串,则按字符串模式编码(utf-8/ASCII)...
字符串类型的最大空间不能超过512m,(value不大于512)
操作:
- msetnx:是一个原子性操作,有一个失败则全部失败
String底层数据结构——简单动态字符串(Simple Dynamic String SDS)内部结构实现类似于ArrayList,采用动态扩容方式减少不必要的内存分配。
Redis 原子操作
指 不会被线程调度打断的操作
在Redis中由于是单线程,于是任何一条命令都是原子操作,因为线程中断只能发生于指令之间。
但在多线程中,命令存在被打断的可能,不会被其他线程切的操作即为原子操作。
引理:
Java中的原子性分析:
关键逻辑:Java中的一条指令底层是由几条原子性指令组合的,各个原子性指令如果被其他线程插足会有什么影响。
List列表类型命令
底层:
-
List是单键多值结构
-
会按照插入顺序排序,也可以向头/尾添加元素
-
底层是一个双线链表
实质上是链表+列表/数组的形式(hashmap的转置)
而每个节点并不是一个value,而是一个压缩列表
命令:
Set
命令:
底层数据结构:
哈希表实现字典
Hash类型
表示k-v中v是哈希表
适合表示对象类型/JSON嵌套类型的数据
且此数据结构支持套娃——能够完全支持对象的表示
命令:
底层数据结构:
Hash类型的实现数据结构有两种
- 当field-value长度较短/个数较少时,使用ziplist
- 否则使用hashtable
怎么实现套娃——简化结构 | 退化结构
Redis将value中的map转化成(序列化)字符串作为真实的value,解读时再解析出需要的格式。
外部哈希的值字段可以存储其他数据类型(如嵌套的哈希)的序列化字符串,但外部哈希自身通常不需要被序列化为字符串。外部哈希的键和值在 Redis 内部都以二进制形式存储,Redis 会负责管理这些数据的内存布局和编码方式。
有序集合 Zset(sorted set)
Zset中存储没有重复的字符串集合(set),但区别在于,每个成员都关联一个评分(score),评分从低到高排序集合中的成员。
集合中成员是唯一的,但评分是可以重复的。
命令:
SortedSet 底层数据结构:
底层使用了两个数据结构:
-
Hash:
field是本体,value是score
-
跳跃表:
基础链表+辅助链表 完成查找
哈希表和跳跃表同时存在,每一步crud都要回鹘哈希表和跳跃表,尤其要保证跳跃表中crud之后任然有序
跳跃表查找的逻辑:
小二分法——通过先检查远端某个值的大小来二分出左右
但主要因为是链表,不太能够直接获取中间位置的值,顾用二分法比较有局限。
Bitmaps 位图
-
位图本身不是一种数据结构,实际上是字符串,但特殊的格式可以让其进行位运算。
命令:
功能:
性能比较:
- 位图更适合大规模的布尔状态,及能够节省内存,同时能进行高效的位操作
- 如果用集合实现,key为身份,value是bool,由于set底层用hashmap,默认空间就会导致额外的空间浪费。
HyperLogLog
场景描述:
-
业务需要统计访问网站的具体用户的数量,而能简单得到的是每次对网站的访问,需要根据用户对访问次数进行筛选 —— 完成去重、计算等工作。
-
解决方案:
- 数据存储在Mysql中,用distinct count 计算不重复的个数
- 数据存储在Redis中,使用hash,set天然去重,或用bitmaps位操作筛选
以上解决方案的问题:随着数据量增大导致占用空间过大
HyperLogLog:
是一种用于估计集合基数的概率性数据结构
能够在非常小的内存消耗下,近似完成基数估计
是在精确度和内存消耗之间的权衡,适用于那些对精确性要求不是非常高的场景。
命令:
Geospatial
是一种用于处理地理空间信息的数据类型。拥有一组执行地理空间操作的命令。
命令:
Redis中的发布与订阅
发布订阅(pub/sub)是一种消息通信模式:
发布者(pub)发布消息,订阅者(sub)接收消息
Redis客户端可以订阅任意数量的频道
语法:
订阅:
发布:
发布成功:
接收:
Jedis
目的是用Java程序远程操作Redis,具体方法是建立IP+端口的连接,传递命令
类似JDBC,感觉都有RPC的意思
语法:
实例:
用redis+Jedis实现收集验证码
我的想法:用一个hash结构,key是用户,value的哈希表中存储次数、每次的验证码...但是问题是:过期只能整个k-v过期,除非用逻辑实现列表/哈希表中去除某个value
于是第二个思路是:k-v仅存储当前的验证码,直接设置过期时间。将次数验证放到Java中执行。
地道的做法:
将hash结构分成多个k-v存入redis,k的结构变为user拼接field | 这就是为什么redis流行拼接【:】
user+"count" 表示次数验证,user+"code" 表示验证码,如此一来所有信息都在库中。
问题:
每个验证码的key都是user+“code”,则必须保证第二次的验证码是在第一次验证码过期后才会入库 —— 在前端设置发送验证码间隔时间。
SpringBoot 整合 Redis
不依赖Jedis,而是StringRedisTemplate。
Redis事务
Redis事务包括一系列命令,在事务的执行过程中,不会被其他客户端发送的命令请求所打断。
理解:Redis通过多路IO复用将多个用户的命令集合成单线程执行,事务操作面临多个用户的命令,实际也就是多线程操作。
基本命令:
-
Multi 开启组队阶段
-
Exec 开启执行组队中的命令
-
discard 放弃组队
事务的错误处理:
-
当组队时一条命令产生错误
全部事务不会执行
linux中每个命令会进行检测,但jedis等执行编译一体时,无法预测是否出错 -
当执行时一条命令产生错误
错误的命令不会执行,其他命令依旧执行
相当于事务的作用只是将一系列命令统一语法检测,过了检测后加锁,分别依次执行
Redis对事务冲突的加锁方案
场景:
多个用户读到存款都是基础数值,试图分别对此数值进行操作,但规定每个用户的操作必须参考之前一个用户操作的结果(余额够不够)
方案一:悲观锁
-
保守的并发控制策略,每次拿取数据时都会悲观地认为别人会修改。于是加锁的程度很大,防止必然到来的篡改。
-
运用在传统SQL数据库中,行锁、表锁、读锁、写锁。
-
流程:一个线程获取锁,执行操作,释放锁。过程中其他线程block
方案二:乐观锁
-
乐观地认为每次操作别人不会更改,先放心地读,如果改了再说。
-
乐观锁适用于多读的应用类型,可以提高吞吐量
-
流程:读的线程会读取到当前的版本号,写的线程会先对比当下的版本号和手里拿的版本号是否相同(有无被其他线程先写过),如果版本号相同,写完后更改版本号,如果版本号不同,无法执行。
具体运用
-
乐观锁
通过将版本号绑定到某个变量,需要得到实时版本号的事务监听此变量,来实现唯一修改
WATCH key
在执行multi之前,先执行watch key1 [key2] 就可以让此multi事务将他一个或多个key
如果在事务exec执行之前,key被其他命令所改动,那么exec将不被执行(discard)。
-
unwatch 取消对某key的监视
Redis事务的三大特性
-
单独的隔离操作
事务中的所有命令都会序列化,按顺序执行。事务在执行的过程中,不会被其他客户端发送的命令所打断。
-
没有隔离级别的概念
MySQL中,事务的隔离级别和程序的并发性反相关
隔离级别越高——事务受其他事务的影响越小——但也牺牲了并发量
-
隔离级别:
-
读未提交:
某个事务对数据的修改无论是否提交,都被其他事务当做真实数据读取
并发性能很高,但问题很明显:脏读、幻读、不可重复读
-
读已提交:
常规理解
-
可重复读:
一个事务内的读取不受其他事务影响
-
串行化:单线程依次执行
-
-
而redis中通过乐观锁的实现,类似MVCC,即读未提交增强版
-
不保证事务的原子性
事务中如果一条命令执行失败(语法正确),其他命令任然会执行
miu sa!
...
Redis 持久化
Redis 提供了两种不同形式的持久化方案
- RDB(Redis DataBase)
- AOF(Append of File)
-
RDB(核心:子线程——异步通信)
在指定的时间间隔内,将内存中的数据集快照写入磁盘
底层实现:
-
Redis会单独创建一个子进程(fork)来处理持久化。
子进程将数据些写入到一个临时文件中,待持久化过程结束后,再用临时文件替换上次持久化好的文件。整个过程中,主进程不进行任何IO操作,保证性能
写时复制技术(fork)
即在主线程执行用户逻辑过程中,将数据持久化到某个文件中
技术要点:Redis复制了与当前进程一样的一个进程,获取到原进程的所有数据(变量,环境变量,程序计数器...),作为其子进程。
-
优点:
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高的更适合使用(最后会丢失)
- 节约磁盘空间
- 恢复的速度快
缺点:
-
写是复制技术需要两个线程,导致内存占用达到两倍。
尤其当频繁写入时,每次都产生一个副本?感觉内存消耗巨高
-
由于RDB的持久化是间隔时间进行的,导致最后一片区的数据可能无法成功写入(写之前宕机了)。
RDB文件包含Redis服务器中的数据、键值对、过期时间等信息快照
-
这表示RDB每次写入.rdb文件时,都会覆盖之前的数据,通过全盘扫描redis服务器获取最新数据,以此覆盖旧信息
这就表示更新太频繁没必要,更新太慢会有最后一把无法及时更新的弊端。
-
AOF(核心:单线程同步通信)
以日志的形式来积累每个写操作(增量保存),将Redis执行过的所有写指令记录下来,只需追加文件,但不可改写文件,redis启动之初会读取改文件重新构建数据——会将日志中的指令全部执行一遍来完成数据恢复。
Redis启动会默认读取RDB和AOF的持久化文件,但当二者都启动时系统默认读取AOF的数据。
AOF的执行过程
三种模式:appendfsync:XXX
-
always:
每当一句指令执行就用主线程同步写入.aof文件,收到同步确认之前,用户端类似阻塞(其实是线程忙写入去了)
-
everysec:
每一秒写一次,也要求同步确认(表明AOF也存在缓存区)
-
no:
写是写,不不要求同步确认
-
三种模式是数据持久性和redis性能的平衡
AOF文件过大时的操作:
重写:
- 也是先写临时文件然后rename
- redis也通过fork一个子线程来重写文件,将可以合并的各种繁琐命令聚合——只关注最终结果,不关注过程
主从
主机、从机
实现的功能为:从主机写入的kv,可以在从机读取到
Redis集群
redis集群实现对redis的水平扩容,当启动 n 个redis阶段,整个系统将数据分布式地存储到 n 个节点上,每隔节点存储总量的1/n。
Redis集群的目标是将数据分片,每隔分片分布在不同的Redis节点上,从而实现负载均衡
服务器集群
由于主要作用并非存储数据,而是运算。
通常指将多个独立的服务器或计算机节点组合,也是为了负载均衡,提高性能
Redis问题以及解决方式
-
缓存穿透
当某时刻应用服务器访问量很大,但redis并没有起效,导致服务器依然次次访问数据库
可能的情况:
- 服务器每次都试图查询数据库并将结果保存在内存中,但当查不到时自然无法保存到redis,反而白白执行了每次的查库操作。
解决方案:
-
对空值进行缓存
当一个查询的结果为空,为了防止其在此多次查询,可以将此查询对应一个null进行缓存。
问题:
- 当查询是多变的,此方法很耗内存且没什么用
-
设置可访问的名单(白名单)
使用bitmaps位图定义一个可访问的名单,每次访问都要通过bitmap比较以此,如果不在bitmap中,则进行拦截
-
布隆过滤器
对bitmap位图优化性能,但降低准确度
-
缓存击穿
当对某一个在缓存中不存在的数据进行超高并发量的访问时,这些访问发现redis中没有,就都向数据库进行访问了
可能的情况:
- 热点数据刚刚过期就迎来高并发访问
- 黑客恶意攻击某一不在缓存中的数据
解决方案:
-
预先设置热门数据
-
实时调整,线程监控哪些数据热门,调整其key的过期时间
-
使用锁:
由于问题来自并发,使用锁一个个来自然可以
给第一个访问的加锁,第一个把数据放入内存后就解锁,感觉应该可以⑧
-
缓存雪崩
当在极少时间内,缓存中大量key过期,导致查询突然涌到数据库
可能的情况:
- 多个key有相同的超时时间
解决方案:
- 构建多级缓存结果(上游nginx配缓存,下游再配其他缓存)
- 使用锁 / 队列
- 设置过期标志更新缓存
- 让缓存失效时间分散