文章目录
主要内容
引入
Web1.0的时代,数据访问量有限,使用高性能的单点服务器可以解决大部分的问题。
随着Web2.0的到来,用户访问量大幅度提升,同时产生了大量的用户数据,加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战。
解决CPU及内存压力
解决IO压力
NoSQL数据库
概述
非关系型的数据库,NoSQL不依赖业务逻辑方式存储,而以简单的key-value
模式存储,大大增加了数据库的扩展能力。
特点
- 不遵循SQL标准
- 不支持ACID
- 远超于SQL的性能
适用场景
- 对数据的高并发的读写
- 海量数据的读写
- 对数据的高扩展性
例子
Memcache
- 很早出现的NoSQL数据库
- 数据都在内存中,一般不持久化
- 支持简单的
key-value
模式,支持类型单一 - 一般作为缓存数据库辅助持久化的数据库
Redis
- 几乎覆盖了Memcached的绝大部分功能
- 数据都在内存中,支持持久化,主要用作备份恢复
- 除了支持简单的
key-value
模式,还支持多种数据结构的存储,比如:list、set、hash、zset等。 - 一般作为缓存数据库辅助持久化的数据库
MongoDB
- 高性能、开源、模式自由的文档型数据库
- 数据都在内存中,如果内存不足,把不常用的数据保存到硬盘中
- 虽然是
key-value
模式,但是对value
提供了丰富的查询能力 - 支持二进制数据以及大型对象
- 可以根据数据的特点替代RDBMS,成为独立的数据库,或者配合RDBMS,存储特定的数据。
Redis概述
Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存、事件发布或订阅、高速队列等场景。支持网络,提供字符串、哈希、列表、队列、集合结构直接存取,基于内存,可持久化。
什么是Redis
Redis是一款内存高速缓存数据库,全称为Remote Dicomtionary Server
(远程数据服务),使用C编写,是一个键值存储系统,支持丰富的数据类型,如:string、list、set、zset、hash
。
特点
-
读写性能优异
-
数据类型丰富
-
原子性
Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。 -
丰富的特性
Redis支持publish、subscribe
、通知、key过期等特性。 -
持久化
Redis支持RDB、AOF等持久化方式 -
发布订阅
-
分布式
Redis cluster
使用场景
热点数据缓存
缓存是Redis最常见的应用场景,主要是因为Redis读写性能优异,逐渐取代memcached,成为首选服务端缓存的组件,而且Redis内部是支持事务的,在使用的时候能有效的保证数据的一致性。
作为缓存使用时,一般有两种方式保存数据:
- 读取前,先去读Redis,如果没有数据,读取数据库,将数据拉入Redis。
- 插入数据时,同时写入Redis。
方案一:实施起来简单,但是有两个需要注意的地方:
- 避免缓存击穿。(数据库没有需要命中的数据,导致Redis一直没有数据,而一直命中数据库????)
- 数据的实时性相对会差一点
方案二:数据实时性强,但是开发时不便于统一处理。
方案一适用于对数据实时性要求不是特别高的场景,方案二适用于字典表、数据量不大的存储系统。
限时业务的运用
redis可以使用expire
命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。
计数器相关问题
redis由于incrby
命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。
分布式锁
主要利用redis的setnx
命令进行,该命令就是如果不存在则成功设置缓存同时返回1,否则返回0。因为我们的服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先通过setnx
设置一个lock,如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。分布式锁主要用在秒杀系统中。
延时操作
举例:在订单生产后我们占用了库存,10分钟后去检验用户是否真正购买,如果没有购买将该单据设为无效,同时还原库存。由于redis自2.8.0之后版本提供Keyspace Notification
功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响resdis数据集的事件。所以我们对于上面的需求就可以直接用下面的解决方案:我们在订单生产时,设置一个key,同时设置10分钟后过期,我们在后台实现一个监听器,监听key的实效,监听到key实效时将后续逻辑加上。
当然我们也可以利用rabbitmq、activemq
等消息中间件的延迟队列服务实现该需求。
排行榜的相关问题
关系型数据库在排行榜方面查询速度普遍较慢,所以可以借助redis的SortedSet
进行热点数据的排序。
比如点赞排行榜,用SortedSet
进行实现,然后以用户的openid作为上面的username,以用户的点赞数作为上面的score,然后针对每个用户做一个hash,通过zrangebyscore
就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息。
点赞、好友等相互关系的存储
redis利用集合的一些命令,比如求交集、并集、差集等。在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
简单队列
由于redis有list push和list pop这样的命令,所以能够很方便的执行队列操作。
Redis技术
Redis是单线程+多路复用技术。
多路复用是指用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select或poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程中执行,也可以启动线程执行(比如使用线程池)。
常用的五大数据类型
对redis来说,所有的key都是字符串,我们在谈数据结构的时候,讨论的都是存储值的数据类型,主要包括五种,主要是String、List、Set、Zset、Hash。
String字符串
String是中最基本的类型,一个key对应一个value,String类型是二进制安全的,意味着Redis的String可以包含任何数据,比如:jpg图片或者序列化的对象。
实战场景:
- 缓存:经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
- 计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
- session:常见方案spring session+redis实现session共享。
原子性:所谓的原子操作是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何context switch
(切换到另一个线程)。
- 在单线程中,能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间。
- 在多线程中,不能被其他进程(线程)打断的操作叫原子操作。
Redis单命令的原子性主要得益于Redis的单线程。
String的数据结构为简单动态字符串(SDS),是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。
如图所示,内部为当前字符串实际分配的空间capacity
一般要高于实际字符串的长度len
。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M.
List列表
单键多值,Redis中的List就是一个链表,链表上的每个节点都包含一个字符串。
使用List结构,可以轻松的实现最新消息排队功能。List的另一个应用就是消息队列,可以利用List的PHSH操作,将任务存放在List中,然后工作线程再用POP操作将任务取出进行执行。
Redis列表是简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部或者尾部。
底层实现是一个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
List的数据结构为快速链表quickList
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist
,即压缩列表。他将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量较多的时候才会变成quickList
,因为普通的链表需要的附加指针空间太大,会比较浪费空间。
Redis将链表和ziplist
结合起来组成了quickList
,也就是将多个ziplist
使用双向指针串起来使用,这样既能满足了快速的插入删除性能,又不会出现太大的空间冗余。
Set集合
Redis的Set是String类型的无序集合,集合成员是唯一的,不能出现重复的数据。并且set提供了判断某个成员是否在一个set集合内的重要接口,这个是list所不能提供的。
实战场景:
- 标签(tag):给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
- 点赞,或点踩,收藏等:可以放大set中实现。
Redis的Set底层其实是一个value为null的hash表,所以添加、删除、查找的复杂度都是O(1)。
Set数据结构是dict字典,字典是用哈希表实现的。内部也使用hash结构,所有的value都指向同一个内部值。
Hash散列
键值对集合,是一个String类型的field(字段)和value(值)的映射表,hash特别适合用于存储对象。主要结构为:
通过key(用户ID)+field(属性标签)
就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
实战场景:
缓存:能直观的维护缓存信息,如用户信息,视频信息等,相比String节省空间。
Hash类型对应的数据结构是两种:ziplist(压缩列表)、hashtable(哈希表)。当fileld-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
Zset(sorted set)有序集合
与普通的set非常相似,也是String类型元素的集合,是一个没有重复元素的字符串集合。不同之处是有序集合的每个元素都关联了一个double类型的评分(score),这个评分被用来按照从最低分到最高分的方式排序集合内的成员。集合的成员是唯一的,但是评分可以是重复的。
实战场景:
排行榜:经典使用场景。
zset底层使用了两个数据结构:
(1)hash:hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表:跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
跳跃表
有序集合比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便于元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。
三种特殊类型详解
HyperLogLogs(基数统计)
能够解决的问题:
可以非常省内存的去统计各种计数,比如注册IP数、每日访问IP数、页面实时UV、在线用户数、共同好友数等。
Bitmap(位存储)
都是操作二进制位来进行记录,只有0和1两个状态。
能够解决的问题:
比如:统计用户信息,活跃&不活跃;登录&未登录;打卡&未打卡;两个状态的,都可以使用Bitmap
geospatial(地理位置)
可以推算地理位置信息:两地之间的距离,方圆几里的人。底层的实现原理实际上就是Zset,可以通过Zset命令来操作geo。
geoadd
:添加地理位置geopos
:获取指定成员的经度和纬度geodist
:获取当前定位,一定是一个坐标值,如果不存在,返回空。georadius
:获取所有附近的人的地址,定位,通过半径来查询。georadiusbymember
:显示与指定成员一定半径范围内的其他成员。geohash
:返回11个字符的hash字符串,将二维的经纬度转换为一维的字符串,如果两个字符串越接近,则距离越近。
感谢并参考:
https://pdai.tech/md/db/nosql-redis/db-redis-data-type-special.html