这里写目录标题

  • 关系型和非关系型数据库
    • 关系型数据库
    • 非关系型数据库
      • 非关系型数据库的好处
      • 非关系型数据库的类型
  • redis数据类型和编码格式
    • 数据类型
    • 编码格式
    • string类型为什么是二进制安全的
      • 为什么是sds数据类型
      • 为什么empstr编码和raw编码的区分44?
    • Redis高性能
      • 纯内存存储数据
      • 采用epoll的多路复用的NIO技术
      • 单线程
    • Redis可以做什么
    • 分布式锁
      • 如何实现分布式锁
      • 锁失效的原因
      • 如何解决锁失效
  • 总结

关系型和非关系型数据库

关系型数据库

关系型数据库是数据与数据之间存在相互关系的二维存储结构,比如主键关联和外键关联等,比如书的类型与书之间的关系,学生与班级之间的关系。它支持原子性的事务操作,可以纵向持久化到磁盘上,以防止数据丢失。

但是,它一旦建立了关联关系的表结构,修改是比较麻烦的。同属,如果达到了数据存储的瓶颈,它只能向上扩展,比如升级硬件。当然,当然也可以横向存储,比如分表分库,但这里需要复杂的技术,比如通过mycat进行分库分表。如果存储了大量地数据,磁盘地读写压力比较大,如果存在关联表的查询,查询效率很慢,极大地影响到用户体验。

非关系型数据库

非关系型数据库的好处

关系型数据库的优点有,但也不能避免它带来的弊端,因而出现了非关系数据库,即NoSQL「not only sql」。它不存在表与表之间的关联关系,可以有很好的水平扩容,支持海量数据的存储,支持分布式架构,但不支持事务。它一般存储音频、视频,文档等文件。

非关系型数据库的类型

  1. 列式存储:Hbase,抓取增量数据
  2. Kv形式:redis[lua原子操作]
  3. 文件存储:mongoDB
  4. 图【节点】存储:neo4j
  5. 。。。

这里主要讲解非关系型数据库中的Redis数据库。

redis数据类型和编码格式

数据类型

redis全名为remote dictionary server 远程字典服务,既然字典服务,其存储样式类似于map,这种键值对。既然是存储数据,便有特定的数据类型。

redis最常用的有五种数据类型,以下表格来源于菜鸟教程:

数据类型 简介 特性 适用场景
String(字符串) 二进制安全 可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M
Hash(字典) 键值对集合,即编程语言中的Map类型 适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去) 存储、读取、修改用户属性
List(列表) 链表(双向链表) 增删快,提供了操作某一段元素的API 1,最新消息排行等功能(比如朋友圈的时间线) 2,消息队列
Set(集合) 哈希表实现,元素不重复 1、添加、删除,查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作 1、共同好友 2、利用唯一性,统计访问网站的所有独立ip 3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐
Sorted Set(有序集合) 将Set中的元素增加一个权重参数score,元素按score有序排列 数据插入集合时,已经进行天然排序 1、排行榜 2、带权重的消息队列

除以上5种数据类型,还有其他数据类型,比如记录地理位置geospatial类型。

编码格式

每种数据类型都对应这不同的编码格式,而编码格式只能从小内存转化为大内存,不能由大内存转化为小内存,过程不可逆,这里和jvm的动态堆内存还是不同的。比如 empstr只能升级为raw编码,而不能由raw编码转为empstr编码。

以下表格是五种数据类型对应的11种编码格式:

类型 编码 对象
String int 整数值实现
String embstr sds实现 <=44 字节
String raw sds实现 > 44 字节
List ziplist 压缩列表实现
List linkedlist 双端链表实现
Set intset 整数集合使用
Set hashtable 字典实现
Hash ziplist 压缩列表实现
Hash hashtable 字典使用
Sorted set ziplist 压缩列表实现
Sorted set skiplist 跳跃表和字典

接下来,重点说说sas的empstr和raw这两种编码格式。

string类型为什么是二进制安全的

为什么是sds数据类型

我们都知道redis底层是使用C语言写的,C语言是没有字符串类型的,因而,它把字符串存储到char数组之中。但是,C语言读取字符串时,一旦遇到 \0,便终止读取字符串,如下代码:

#include <stdio.h>int main () {printf("hello word \0 hello word \0 hello word ");return 0;
}

输出结果是:

如果存储图片的二进制,肯定会有结束符 \0,此时,图片就不完整,因而,会出现编码的问题。
此外,C语言每次读取字符串,都要重新分配内存空间,这就在分配和回收上浪费了性能。

因而,这就让Redis开发者,不得不重新定义新的数据类型,动态扩展字符串,因而出现了sds的数据类型,全程 simple dynamic string,简单动态字符串。

sds采用预分配内存空间的方式,这就很好地避免重复分配内存所带来的性能消耗,这也是Redis性能高的一个原因。

当我执行以上set 命令时,Redis会创建两个对象,键对象和value对象,键对象是16字节的redisObject类型,值可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种, 是归根到底是sds类型,如图所示***【备注,引用网上的图片】***:

如果key值和value存储在同一个内存空间, Redis只需要分配一次内存即可,这里就需要一种编码格式,即empstr编码格式,怎么判断在不在同一内存空间,这里有个参数为44,之前为39字节

为什么empstr编码和raw编码的区分44?

/* object.c */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 //制定长度struct __attribute__ ((__packed__)) sdshdr8 {//当前字符数组的长度uint8_t len; /* used */
//    当前字符数组总共分配的内存大小uint8_t alloc; /* excluding the header and null terminator */
//    当前字符数组的属性、用来标识到底是 sdshdr8 还是 sdshdr16 等unsigned char flags; /* 3 lsb of type, 5 unused bits */
//    字符串真正的值char buf[];
};

redis使用jemalloc内存分配器,分配8,16,32,64等字节的内存。假设key值和value在同一内存空间,key为16个字节,value值中 len 为 2个字节,flags为1个字节,同时 \0为1个字节,
64=RedisObject+len+flags+结束符=16+2+1+1+44=64;

因而,同一内存最大分配空闲内存为44个字节,这也是划分empstr和raw编码格式的依据。如果字符超过44个字节,编码格式为raw类型。反之,编码格式为empstr,但是,如果empstr修饰的数据发生改变,数据编码直接变为raw类型。

综上所述,string如果获取当前字符的长度,直接通过 len 就能获取,时间复杂度为O(1),加上预分配内存大小,并且很好地规避了结束符 \0,这就让string数据类型不仅安全,而且高效。

这也是他高可用的一个条件,当然,条件不止这一种。

Redis高性能

纯内存存储数据

Redis将所有的数据均存储的内存当中,同时采用sds的预分配内存空间的原理,使得内存响应时间大约为100纳秒,从而达到每秒万级的访问数量。

采用epoll的多路复用的NIO技术

NIO non-block I/O 非阻塞I/O,这是相对于传统的组设I/O来说。传统的BIO会有建立阻塞和读写阻塞的问题,因而需要引入非阻塞IO吗,非阻塞IO采用ChannelServerSocket的方式处理。

socket采用tcp协议以轮询的方式ChannelServerSocket通信,此时ChannelServerSocket返回一个状态给socket,比如该状态是可读还是可写。假如socket1未建立连接,会以自旋的方式不断发送请求,直到建立连接为止,这种方式是比较耗资源。

因而,在服务端采用池化技术,开启多个ChannelServerSocket,socket向ChannelServerSocket1发出请求,ChannelServerSocket1返回不可用的状态,此时,socket向ChannelServerSocket2发送请求,ChannelServerSocket2返回可用的状态,于是建立可读或可写的连接,如果所示:


这种也是select或poll的模型,但是其还是采用自旋的方式建立连接,这样在高并发的情况下,也会存在带宽消耗的。select有最大连接数的限制,但poll没有最大连接数的限制。

有没有一种在服务端事件监听的机制,socket向服务端发送请求,监听器监听到哪个或哪些ChannelServerSocket是可用的,并主动与socket建立可读或可写连接,这样可以规避自旋所带来的资源消耗,但是,监听是在服务端资源消耗,有舍有得,这就是epoll模型。

而Redis正式采用epoll模型来实现高性能的多路复用。

单线程

Redis是采用单线程的技术,避免了多线程环境下的竞态机制和上下文切换所带来的性能消耗。

Redis可以做什么

由于Redis高性能的机制,可以用它来做以下事情:

  1. 缓存,使用Redis做缓存,在提升服务器性能方面非常有效;
  2. Redis可以做分布式锁。
  3. 排行榜。若使用传统的关系型数据库来做的话,比较麻烦,但利用Redis的zsett能够轻松搞定;
  4. 计数器。Redis支持lua脚本的原子性操作,因而,利用Redis中原子性的自增操作,可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;
  5. 限流。限制某个API的访问频率,比如有抢购时,防止用户疯狂点击带来不必要的压力。比如,我的博文对外api架构设计就利用了这一点。
  6. 好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;
  7. 简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;
  8. Session共享,默认Session是保存在服务器的文件中,即当前服务器,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。

其实,Redis的能做的事情也不止这些,这只是一个简单的例子而已,更重要的是Redis能做分布式锁。

分布式锁

如何实现分布式锁

在实际开发过程中,会遇到下单操作,这里涉及到库存问题。假如现在有一个场景,系统中有一个用户下单接口,但在用户下订单前一定要去检查库存,确保库存足够了才给用户下单,假如目前是单台服务器。由于系统有一定的并发,所以会预先将商品的库存保存在 Redis 中,用户下单的时候会更新 Redis 的库存。

  1. 用户下单
  2. 检查库存
  3. 更新数据库数量
  4. 锁定库存
  5. 继续执行后续代码

假如现在有两台服务器,使用nginx做负载均衡,采用轮训的机制,并采用epoll进行多路复用, 来支撑较高的并发量,架构如下:

用户1和用户2被负载到服务器1,在同一个JVM当中,lock加锁有效,同理,用户3和用户4的lock也成立。假如Redis和mysql存储的库存都是1,用户1执行完第3步后,mysql数据库库存为0,此时,用户3才执行完第二步,从Redis中取出的数据为1,再执行第3步,此时mysql的数据库库存为-1,这就减了两次,造成了超卖的脏数据,因而,这里需要加分布式锁,锁住当前Redis。

分布式锁的思路:在整个系统提供一个全局、唯一的获取锁的“变量”,然后每个系统在需要加锁时,都去问这个“变量”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。能够实现分布式锁的两种途径。

其实,Redis没有锁的概念,但是有一个方法:setNx,set if not exists 该方法说明:如果当前key值存在,不做任何操作,因而,他可以做分布式锁,并设置失效时间,这里容易造成锁失效。

锁失效的原因

假如使用setNx和Expire进行加锁,设置超时时间为10s,使用delete方法进行解锁。现有线程1和线程3访问下单接口,线城1完成任务耗时15S,在第10s秒已超时,锁失效,此时库存值还没有更新到Redis,但数据库库存减掉了。线程3在第10s时获得锁,并修改了Redis和mysql值,这就造成了超卖的异常。

如何解决锁失效

  1. 当线程1获得Redis锁时,便在线程池中创建一个定时任务,每隔3秒去查看线程1是否持有锁,如果持有锁,则增加锁的延迟时间,我们需要额外去维护这个过期时间。不方便。
  2. 使用开源框架,Redission,redission提供简单易用的API,如下加锁代码:
Config config = new Config();
config.useClusterServers().addNodeAddress("redis://192.168.31.101:7001").addNodeAddress("redis://192.168.31.101:7002").addNodeAddress("redis://192.168.31.101:7003").addNodeAddress("redis://192.168.31.102:7001").addNodeAddress("redis://192.168.31.102:7002").addNodeAddress("redis://192.168.31.102:7003");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("stock");
lock.lock();
lock.unlock();

Redisson 所有指令都通过 Lua 原子性的脚本执行。Redisson 设置一个Key 的默认过期时间为30s,它会在你获取锁后,使用 Watchdog ,也就是看门狗,每隔10s帮你把Key的超时时间设为 30s。

总结

这里或许有错误的地方,烦请各位支指出。

Redis的那些事儿:关系型和非关系型数据库,非关系型数据库的类型,redis数据类型、编码格式、高性能、可以做什么、分布式锁失效的原因,string为采用sds数据类型,为什么是二进制安全的,相关推荐

  1. Redis分布式锁失效场景分析

    本文主要介绍了分布式锁失效的三种场景,并通过一些案例进行分析 背景 最近在项目中发现一些因为错误使用Redis分布式锁导致锁失效的问题.在此整理了一下使用分布式锁时要注意的点. 什么是分布式锁 分布式 ...

  2. Redis分布式锁奥义

    分布式锁 分布式系统进行逻辑处理的时候,经常会遇到并发问题,例如直播场景中,用户需要连麦主播,当多个用户在同一个时刻一起连麦时候,应该保证只有一个用户能连麦成功,我们改怎么保证这种业务场景下保证数据的 ...

  3. 【2020尚硅谷Java大厂面试题第三季 04】Redis 9种数据类型使用场景,分布式锁演变步骤,lua脚本,redis事务,Redisson,Redis内存占用,删除策略,内存淘汰策略,手写LRU

    1.安装redis6.0.8 2023 02 02 为:redis-7.0.8.tar.gz 2.redis传统五大数据类型的落地应用 3.知道分布式锁吗?有哪些实现方案?你谈谈对redis分布式锁的 ...

  4. redis应用场景—— 缓存,分布式锁,去重

    Redis实际应用场景 https://www.cnblogs.com/mrhgw/p/6278619.html Redis在很多方面与其他数据库解决方案不同: 它使用内存提供主存储支持,而仅使用硬盘 ...

  5. Redis分布式锁(图解 - 秒懂 - 史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  6. Redis分布式锁解析源码分析

    Redis分布式锁解析&源码分析 概述 实战 简单的分布式锁 Redisson实现分布式锁 Redission源码分析 构造方法 获取锁lock 解锁 锁失效 红锁 案例分析 原始的写法 进化 ...

  7. Redis分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!

    点击关注公众号,Java干货及时送达 来源:juejin.cn/post/6854573212831842311 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本篇文章主要是基于我们实际项目 ...

  8. Redis 分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本 ...

  9. 秒杀商品超卖事故:Redis分布式锁请慎用!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:浪漫先生 来源:juejin.im/post/6854573 ...

  10. 记一次由Redis分布式锁造成的重大事故,避免以后踩坑!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:浪漫先生 juejin.im/post/5f159cd8f2 ...

最新文章

  1. 使用Repeater实现类似GridView编辑功能
  2. HDU - 6194 string string string(后缀数组+RMQ+容斥)
  3. 解决链接模型的可见性问题
  4. 设计模式C++实现 ——观察者模式
  5. uipath循环datatable_UiPath之DataTable转换为List和Array
  6. 企业文化、业务架构与中台:移植阿里的中台战略能成功吗?
  7. __try,__except,__finally,__leave
  8. unix线程死锁概念与解决
  9. Could not create a validated object, cause: ValidateObject failed
  10. oracle 数据库由Linux平台向Windows平台迁移
  11. Linux基于升序链表的定时器
  12. javaScript编码
  13. VSCode使用Vim插件心得
  14. 服务器基础知识大科普
  15. LaTeX中的多行数学公式
  16. MATLAB 车牌识别程序介绍 SVM、神经网络[毕业设计]
  17. 数据库课程设计-宿舍管理系统
  18. 2023电工杯数学建模A题B题思路模型
  19. seaweedfs上传文件为什么要先申请文件号?(/dir/assign)(两种工作模式:Volume模式与Filer模式)(seaweed上传文件)
  20. dell管理工具OMSA

热门文章

  1. 【龙印】用M665和M666给三角洲3D打印机调平
  2. Power BI 客户端 安装 错误
  3. C语言常用的数学函数
  4. 达梦数据库 防火墙设置
  5. 服务器重启后samba启动不了,提示stop: Unknown instance: smbd start/running, process 76585
  6. 微服务网关Gateway基本知识(一)
  7. Python一对一题目辅导「PTA 题目讲解·难度系数:基础」
  8. 【应用安全】“我的QQ被盗,请大家不要相信任何消息.......”
  9. ofbiz UOM Conversion Relationship Not Found [单位转化关系没有找到] 问题解决:
  10. linux uvc协议_UVCCamera-master