这一节我觉得可以用一个词来形容: 讲故事
redis是怎么来的? redis故事的起因, 发展, 高潮, 结局
视野---->先有宽度, 再拓展深度
一. redis的故事
这部分主要讲的是redis是怎么来的. 讲故事的起因和发展
我们把时间轴拉回到很久以前. 没有redis的时候, 我们用什么来存储文件
1. 磁盘存储--全量扫描, 全量IO
在很久很久以前, 数据以文件的形式保存. 这时, 我们要向去读取数据, 可以一行一行的readline, 使用工具可以是grep, awk, java等.
这里的文件保存在哪里呢? 可以保存在磁盘上, 也可以保存在内存上. 但是, 保存在磁盘上和内存上的速度是有天壤之别的.
代码语言:javascript复制常识
在计算机中, 常用的时间的单位
秒, 毫秒, 微秒, 纳秒
磁盘有两个指标:
带宽, 吞吐: 好一点的可以达到百兆的级别, 快一点的机器可以到G的级别, 现在有固态硬盘, 可以到1-3G的级别(比如:pci-e nwme)
寻址时间: 硬盘的寻址时间是毫秒
内存的指标
寻址时间: 内存的寻址时间单位是ns
内存比硬盘的寻址时间快了1000000倍.
代码语言:javascript复制小白和大牛的一个重要区别:
大牛在写代码的时候会考虑io, 考虑性能, 考虑速度
同样写一段程序, 大牛写出来的程序, 比小白写的就快很多. 就是他们始终想各种办法, 找到硬件的优势
代码语言:javascript复制提问: 将一个文件放在硬盘上, 为什么随着文件越来越大, 10M查一个字符串和1G查一个字符串,查找速度是不一样的, 会变慢, 磁盘查找的速度会变慢?
这里一定要get到关键词, 全量扫描, 全量IO
要在硬盘上寻找一个文件, 需要全量扫描整个磁盘, 也就是发生全量io, 而扫描磁盘他的吞吐量是百兆级别, 寻址时间是毫秒级别, 他一次读不完,
所以, 要分批次读取. 不停地循环遍历. 所以, 文件越来越大, 速度越来越慢.
那么如何才能让他变快呢? 这就是新技术出现的前提
2. 数据库的分治和索引
随着时间的推移, 数据库产生了. 数据库给人的感觉是快, 查找数据查起来比磁盘块.
那数据库查询为什么快呢?
有两方面: 1. 数据库有分治的概念, 2. 数据库有索引.
1)数据库分治的概念.
之前数据放在文件里, 如果要去磁盘查找文件, 需要发生全量扫描, 也就是全量io. 数据库的诞生, 一定是要避免全量扫描的发生. 所以, 数据库设计的时候, 设计成了分治的形式.
分成不同的datapage片区, 每个片区4k, 16k等可以设置. 这样, 我保存数据的时候, 将数据放到不同的片区.
比如有一个数据A, 把他放到某一个片区里.
但是, 这时候如果想要去查找数据A, 我还是全量扫描, 因为我不知道数据A在哪里. 这时候就有了另外一个东西--索引
思考: 数据库被划分为多个区, 这些区里面是如何存储数据呢?
数据库建表的时候, 会定义列名, 且每一列是有长度定义的. 而且, 你在放数据的时候, 是按照列顺序存放的.
定义类型: 就是定义了这个字段占的空间
例如: 你创建一个表, 有10列. 每个列占4个字节, 如果这时你只给一个列赋值了, 系统依然会分配40个字节占位. 为什么会这样设计呢?
那就是两种情况的一种是变长存储, 一种是定长存储.
变长存储:
会节省一定的空间, 但是, 需要有一个单独的空间维护记录时间的关系. 而且, 这相当于一个变长数组, 插入和修改数据的时候, 要么记录不连贯, 要么要花费更多时间去扩展空间.
定长存储舍弃了一部分空间的情况下, 让每一行记录,无论有没有数据, 都让他先把位置占上. 不用维护数据之间的关系表. 并且在插入数据的时候, 可以保证插入的顺序. 修改删除也可以保证顺序
2) 索引
上面分析了, 数据库每次保存数据的时候, 会开辟一个全量的空间, 来存储数据, 即是有些列是null. 这样如果我要查找某一个数据值, 就要全量遍历表. 如果数据量越来越多, 那么速度就会越来越慢, 要解决速度越来越慢, 我们建立索引.
索引, 就是标记某一列为索引. 保存的是表中的某一列数据和数据所在的片区. 数据库将索引数据单独存放在一块一个一个的4k的小格子里. 和普通数据一样, 都是存在4k的小格子里. 这时再查询, 我们就避免了在整张表中全量扫描. 但是.......索引数据也是一个全量扫描的过程. 扫描的数据量肯定是比原始数据要小很多的. 不过...依然是全量扫描
数据是数据, 索引也是数据,
把某一列设置为索引, 索引会小于全量数据, 但.....根据索引查询依然是全量扫描. 最大复杂度依然是o(n). 我在查询数据的时候, 根据索引就可以定位到数据了, 索引保证了数据不被全量扫描, 扫描索引, 依然是全量扫描, 索引也是分区的.
索引也是数据, 是数据, 我们就应该想到, 给数据加索引. 很自然, 这时我们就想到给索引在建一个索引. 这就是二级索引.
数据和索引数据都是保存在磁盘里, 磁盘io速度就是会慢. 而且即便是根据索引查找数据, 他的最大时间复杂度依然是o(n)
全量扫描, 这是我们不能接受的, 那么什么快呢? 我们知道内存快, 索引已经是数据大小的1/n了, 索引的索引, 就是索引的1/n, 而且还存在内存中, 内存寻址的速度是ns级别的. 速度自然就快了. 索引的索引保存在内存中, 就像一棵树的根, 索引就像叶子, 通过根找到相应的叶子, 在通过叶子找到数据, 会很快. 比如B Tree, 他只存一个树干. 将索引数据作为子节点, B Tree作为父节点, 查找数据的时候, 根据B tree找到子节点索引, 而且又是在内存中, 速度也就很快了
这也就是说, 如果没有命中索引, 那么就会对整表进行全量扫描.速度自然就会慢了
总结: 数据库为什么快? 数据库进行了分治, 分治还不能完全解决全量扫描的问题, 又有了索引, 索引数据量要比区块数据量少的多, 但索引数据也是全量扫描, 因此又有了内存存储一个B-T结构的父节点数据. 通过父节点, 找到叶子节点也就是索引, 在通过索引找到片区数据. 所以数据查询的速度快了
代码语言:javascript复制问题: 如果数据库的表很大很大, 表的增删改查一定会越来越慢么?
这里要将增删改查定位到io的读写上. 这句话很重要
随着数据量的增大, 增删改一定会变慢, 因为写涉及到索引的维护.
那么读呢? 读分为几种情况
1. 如果只有一个用户, 发来了简单的查询, 带来了一个where条件, 而且where条件刚好命中索引, 这个时候, 他查询的速度是多少? 单位: 秒? 毫秒? 微秒? 纳秒?分?十分? 数据量足够足够大, 几T,几十T
那么读取数据的速度依然是毫秒级别的, 因为他依然走的是B Tree索引->索引-> 数据, 最终数据是磁盘读取的, 磁盘读取的速度是ms级别的, 那么查询的速度不会慢
2. 并发读: 当大量并发产生的时候, 假如并发足够大, 每个人读的数据还都是不一样的. 一个人是4k, 二个人是8k, 四个人是16k, 假设并发足够足够大....这个时候可能就达到磁盘的吞吐的瓶颈了.
磁盘还有一个限制---带宽,吞吐, 数据达到一定程度, 带宽会限制数据的读取速度.
所以, 并发也是对微服务的挑战.
分析了数据库的优缺点:
1. 优点---> 分治, 采用分布式的思想. 分治管理
2. 缺点--->磁盘读取数据, 有寻址和带宽的限制.
如果想规避这个问题, 怎么规避呢?
抛弃磁盘这个瓶颈, 把数据完全放内存里.
二. 全量内存数据库
磁盘有致命的硬伤, 随着数据量越来越大, 文件查找一个资源的速度就越来越慢. 那么就要想办法解决, 然后想到了内存.
就有一家公司发明了内存数据库. SAP HANA是一个全内存的sql数据库. 他把数据全部放在计算机的内存里, 计算机的内存容量是2T, 这样可以足够的快了, 但是就这个2T的内盘容量报价是2亿.
使用全内存的数据库伤不起呀.
全量数据, 存磁盘--> 慢, 存内存-->贵, 怎么办呢? 经过分析, 只有一部分数据是经常使用的数据. 也就是热点数据. 我们只需要把热点数据放在内存里就好了.
于是产生了新的技术, 内存数据库. 比如memecach, redis. 这就是redis的由来.
3. 内存数据库为什么是k-v的?
redis是内存数据库, 且redis是k-v型的. 我们知道其他数据库都是sql型的. redis叫nosql. 那么redis设计的时候被设计成k-v型的呢?
这要从创建redis的人来说起了, 刚开始发明数据库的作者只是想做短域名的映射, 将短域名映射对应的长域名. 且要查询速度快, 还要进行数据快速统计, 他要的是速度.
但是, 任何一项技术, 随着时间的推移, 要么存留, 要么灭亡, 存留下来的也一定是越来越优秀. redis最终存留下来了, 但是没有变成sql的. 依然没有改变k-v的状态, 这一定有他的存在理由和优势.
那么是什么原因呢?
我们来分析sql数据库的特点: 常听到的词汇, 关系, 约束, 范式, 冗余
而redis数据库的特点: 保存热点数据
sql数据库在保存的时候, 表和表之间是有关联关系的. 比如: 表A的一个字段只能是表B里面的值. 那么这个时候, 表B一定需要是全量数据存在, 这样才能保证表A数据的准确性. 但是, redis中保存的是热点数据. 非全量的数据. 破坏了约束的完整性, 而且数据还有可能过期, 老的数据会被挤出去, 这就没有办法保证全映射, 导致映射不全. 所以, redis不能设计成关系型sql数据库.
redis的描述可以参考这个: https://db-engines.com/en/article/Key-value Stores
4. redis是单线程, 还是多线程, 为什么?
redis的woker工作者是单线程, 但是6.3版本以后, 开始支持多线程, 但这里的多线程指的是io threads, 也就是io是多线程的. 但是处理任务, 还是单线程的
5. redis的另一个特点: value是有类型的, 并且有本地方法
redis的value为什么是有类型的?
我们知道redis的value可以是string, list, hash, set, zset. 而且每种类型有自己的本地方法. 那么redis的value为什么是有类型的?并且还有本地方法呢?
这里有一个概念, 叫计算向数据移动, 还是数据向计算移动. redis是计算向数据移动
redis是计算像数据移动, 什么是计算向数据移动呢?
memcache中的value保存的是json格式. 这时有一个客户端, 想要保存一个数组到memcache缓存中. 然后取回数组的第二个元素. 他要怎么操作呢?
存取数据到memcache的步骤
1. 有一个数组[a, b, c, d, e], 保存到memcache中, 将数据的v进行序列化成json字符串, 保存到memcache
2. 取数组中的第二个元素c. 这时, 我们需要将整个数组全部取回,也就是发生全量io, 然后在本地进行反序列化, 然后计算得到第二个元素c
存储数据到redis的步骤
1. 有一个数组[a, b, c, d, e], 保存到redis中. redis中的v有一种类型是list, 直接保存成list的格式.
2. 取数组中的第二个元素c. 这时, 我们直接调用redis的本地方法index(2), 计算会在redis进行, 完成后把c发送回调用者. 调用者不用自己计算.
代码语言:javascript复制区别:
memcache是取回数据自己计算, 这种方式叫数据向计算移动
而redis要那个数据, redis计算好以后, 再返回, 不用我们自己在计算. 这时计算向数据移动
二. redis的应用---秒杀
我们来看一下场景. 现在有个秒杀场景, 有99个商品可以被秒杀. 说到秒杀肯定就有负载均衡了, 有多台服务器做的负载均衡, 然后有多个客户端发送请求, 每台服务器都要去数据库取数据.
这是会有数据同步问题, 服务器都去数据库取数据了, 取回来数据-1, 在保存到数据库, 可能出现三台服务器都取数据,然后计算-1, 再把数据返回给mysql. 这时, mysql产品库存应该剩96个, 结果, 剩了98个. 就会导致多卖. 这对用户的影响是非常不好的. 你都卖出去, 然后告诉他, 不好意思, 没有库存了, 给您退款吧.
这肯定不行, 于是, 我们要想办法保证数据的准确性, 让他们串行去读数据库, 然后库存-1. 这是开启事务. 保证数据的准确性.
本来我们是有多台服务器, 并发处理请求的, 提高系统的处理速度.
但是要访问数据库了, 为了保证数据的准确性, 我们不得不开启事务, 让请求串行化. 串行化其实要做的事情有很多, 每一台服务器都要和数据库获得连接, 然后开启事务, 处理数据, 关闭连接. 然后其他服务器在获得连接, 开启事务, 计算, 关闭事务, 关闭连接. 这中间产生了很多不必要的损耗和浪费
这时, 如果我们巧妙的把mysql, 换成redis
首先. redis是单线程的. 多个数据请求过来了, 他要排队处理.
第二. redis有本地方法. 计算向数据移动. 他可以自己进行逻辑运算, 把运算结果吐出来
第三: redis是内存数据库, 性能是有天壤之别的.
这时redis使用的场景之一
通常, redis排队处理请求, 但是订单最终还是会在数据库中保存
这样就要去访问数据库, 我们在秒杀中一定要做的一件事是: 防止超卖, 削峰
如何防止削峰呢?
因为秒杀价格便宜, 极具吸引力, 我们会有限制, 每人只能下一单, 一定会有人通过刷单的方式恶意下单. 或者有恶意的流量, 不健康的流量请求过来. 这时, 我们要拦截住这些流量. 不能让这些流量请求访问我们的数据库. 这时就有了redis在秒杀中的第二个应用场景.----- 拦截恶意流量
因为redis是内存数据库, 很快, 所以, 可以对用户的流量或ip做一个filter过滤.
这样, 请求了10000个, 有效请求只有1000个, 剩下的9000个连server服务器都进不来. 这是纵向控制了流量, 只让有效的流量进来.
那么我们横向也要进行控制, 比如:现在有1000个有效流量进来了,但是, 我的商品只有99个. 我们要控制超卖, 如何控制. 我们来看看某米是如何做的
我们会有一个队列, 比如kafka队列, 过来的前99个流量放入到队列中, 去等待和数据库交互. redis做库存减1的操作, 一旦redis的库存为0了, 立刻告诉nigx, 库存为0 , 已经卖完了. nginx端给出一端js, 卖完的提示, 然后新的流量进都进不来了
redis在拦截的两个用途, 1. 拦截恶意请求. 2. 库存为0 ,拦截用户的请求.
一个秒杀, 前期要做很多事情.
比如: 第一个, 客户端预热, 比如,京东, 淘宝, 在双11之前, 就要把页面都缓存在客户端了. 或者把所有数据打到cdn上. 优惠券. 通过优惠券预估流量值.
本节课的完整笔记: