Redis是一种NoSQL数据库(多用于解决解决高并发、高可用、高可扩展、大数据存储问题。NoSQL可以作为关系型数据库的良好补充,但是不能替代关系型数据库。)
高性能、高并发:
①使用内存存储操作,没有磁盘IO上的开销
②单线程,省去了上下文切换的开销(redis6.0之后有多线程功能,只针对写操作启用IO线程,如果读操作也需要的话,需要在配置文件中进行配置,且后台线程主线程还是单线程)
③ I/O 多路复用
解决非阻塞IO(减少线程的大量使用)不断轮询导致CPU占用升高的问题,出现了IO复用模型。IO复用中,使用其他线程帮助去检查多个线程数据的完成情况,提高效率。select(查询)、poll(轮询)和epoll(事件驱动)三种方式来实现IO复用,将空闲的io线程阻塞掉,轮询处理那些就绪的流。
④合理高效的数据结构
高可用:主从复制、哨兵机制、持久化
若想了解Redis的安装使用可以参考:https://blog.csdn.net/tttalk/article/details/122042206
常见NoSQL数据库分类:
分类 | 相关产品 | 应用场景 | 数据模型 | 优点 | |
---|---|---|---|---|---|
键值存储数据库 | Redis、Memcached | 内容缓存,如会话、配置文件、参数等;频繁读写、拥有简单数据模型的应用 | 键值对 | 扩展性好,灵活性好,大量操作时性能高 | |
列存储数据库 | Cassandra(AP)、HBase(CP)、Clickhouse | 分布式数据存储与管理 | 以列族式存储,将同一列数据存在一起 | 可扩展性强,查找速度快,复杂性低 | |
文档型数据库 | MongoDB | Web 应用,存储面向文档或类似半结构化的数据 | 键值对,不过值是 JSON 结构的文档 | 数据结构灵活,可以将所需要的数据以内嵌文档形式存储 | |
图形数据库 | Neo4J、InforGrid | 社交网络、推荐系统,专注构建关系图谱 | 图结构 | 支持复杂的图形算法 | |
时序数据库 | influxDb | 分析时序数据 | 处理指标数据的聚合,并且读写效率非常高 | ||
搜索引擎存储 | Elasticsearch、Solr | 搜索引擎,全文搜索 | 基于lucence | 全文搜索 |
redis有五大数据类型:String、Hash、List、Set、SortedSet(zset)
redis不同类型的使用场景:
①字符串
a) 缓存功能
加速读写和降低后端压力的作用。
推荐的方式是使用“业务名:对象名[属性]”作为键名,可以在能描述含义的前提下适当减少键的长度。
b) 计数
快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。
c)保存常用码值
d) 限速
为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。
②哈希
a) 缓存用户信息
对用户的Session进行集中管理,哈希类型更加直观,更新操作会更加便捷。
哈希类型和关系型数据库不同之处:
哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL)。
3、列表使用场景
a) 消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的"抢"列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
b) 文章列表
每个用户有属于自己的文章列表,现在需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
c) 开发提示
lpush + lpop = Stack(栈)
lpush + rpop = Queue(队列)
lpush + ltrim = Capped Collection(有限集合)
lpush + brpop = Message Queue(消息队列)
4、集合
a) 标签(tag)
集合类型比较典型的使用场景是标签(tag),例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣就是标签。 开发提示:用户和标签的关系维护应该在一个事物执行,防止部分命令失败造成的数据不一致。
5、有序集合
a) 排行榜系统
有序集合比较典型的使用场景就是排行榜系统,例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。
下面介绍这几种类型的数据结构:
1.String
(1)常用语句:get/set/mset/del/incr/append/strlen(字符串长度)
(2)数据结构与java中字符串区别:
①java中1.8之前是char数据,之后是byte数组;
②Redis中字符串是动态字符串SDS(simple dynamic String),类似于arraylist。
sds里面定义了三个变量,free:剩余空间;len:字符串长度;buf:存放的字符数组。
为了减少修改字符串带来的内存重分配次数,使用了空间预分配:拓展的内存比实际需要的多;惰性空间释放:sds在数据减少时不会立刻释放空间。
2.Hash
(1)常用语句:hset/hget/mset/hexists/hkeys/hvalues/hlen( 例:hmset user age 20 username lis)
(2)数据结构
有ziplist压缩列表和哈希表两种数据结构:当列表对象有字符串长度64字节或元素数量大于512时,会转化成哈希表
压缩列表的原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存(减少了内存碎片)。
3.List
(1)常用语句:lpush(左)/rpush/lpop/rpop/lrange/lindex(例:lrange list:1 0 2 – index0到2的数据)
(2)数据结构
有ziplist和双向链表两种数据结构:
当列表对象有字符串长度64字节或元素数量大于512时(转化条件同Hash),会转化成双向链表
4.Set
(1)常用语句:sadd/srem/smembers/sismember/sunion(并集)/sdiff(差集)/sinter(交集)
(2)数据结构
有inset和哈希表两种数据结构:
Inset:有序不重复的整数型的数组
当元素数量大于512,或有一个元素不为整数,会转换成哈希表
5.SortedSet(有序的set)
(1)常用语句:zadd/zrange(小到大)/zrerange(大到小)/zscore
(2)数据结构
有ziplist和跳表两种数据结构:
当元素数量大于128,或有元素长度大于64,转换为跳表
跳表:一种基于线性表实现简单的搜索结构, 其最大的特点就是: 实现简单, 性能能逼近各种搜索树结构.
1.架构细节:
(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
(2)节点的fail是通过集群中超过半数的节点检测失效时才生效.
(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
2.redis-cluster投票:容错
(1)集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信超过(cluster-node-timeout),认为该master节点挂掉.
(2)什么时候整个集群不可用(cluster_state:fail)?
如果集群任意master挂掉,且当前master没有slave,则集群进入fail状态。也可以理解成集群的[0-16383]slot映射不完全时进入fail状态。
如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。
(3)redis不能保证一致性
当primary node失效时,slave node有机会被提升为primary node。但由于它们之间是异步传输,因此slave node被提升为primary node后会导致0~N秒的数据丢失。
1.RDB方式(默认采用)
RDB方式的持久化是通过快照(snapshotting)完成的,当符合一定条件时Redis会自动将内存中的数据进行快照并持久化到硬盘。
缺点:一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。
2.AOF持久化
默认情况下Redis没有开启AOF(append only file)方式的持久化
可以通过修改redis.conf配置文件中的appendonly(yes)参数开启
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬
盘中的AOF文件。
1.为什么需要主从复制
持久化保证了即使redis服务重启也不会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,不过通过redis的主从复制机制就可以避免这种单点故障.
2.主从复制的实现
(1)主redis中的数据有两个副本(replication)即从redis1和从redis2,即使一台redis服务器宕机其它两台redis服务也可以继续提供服务。
(2)主redis中的数据和从redis上的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上。
(3)只有一个主redis,可以有多个从redis。
(4)主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求
(5)一个redis可以即是主又是从
配置:修改从服务器上的redis.conf文件
# slaveof
1.每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
2.如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)。
当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)。
若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。
3.当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
1.Redis的事务是通过MULTI(开始放入队列),EXEC(执行),DISCARD(清除队列)和WATCH(监控)这四个命令来完成的。
2.Redis事务介绍
(1)Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。
(2)Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
(3)Redis不支持回滚操作:
①大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
②redis为了性能方面就忽略了事务回滚
3.分布式锁
单应用和分布式应用使用锁的区别:前者单进程多线程(synchronize、Lock),后者多进程
分布式锁的注意事项:
互斥性:在任意时刻,只有一个客户端能持有锁
同一性:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
避免死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
分布式应用使用锁实现方式(性能从低到高):
(1)基于数据库的乐观锁:每个表设计一个版本号字段,写一条判断 sql 每次进行判断
(2)基于zookeeper的分布式锁:上锁改为创建临时有序节点,判断只有序号最小的可以拥有锁,如果这个节点序号不是最小的则watch序号比本身小的前一个节点,继续判断。
(3)基于redis的分布式锁(基于缓存):set获取锁 del释放锁。
jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)
当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。已有锁存在,不做任何操作
1.队列模式:使用list类型(类似于MQ的队列模型:任何时候都可以消费,一条消息只能消费一次)的lpush和rpop实现消息队列。
消息接收方若不知道队列中是否有消息,会建立连接一直发送rpop命令,占用资源。
可以使用brpop命令,若队列中取不出来数据,会一直阻塞,在一定范围内没有取出则返回null。
2.发布订阅者模式:包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或者多个频道(channel),而发布者可以向指定的频道(channel)发送消息,所有订阅此频道的订阅者都会收到此消息。通过Subscribe/unsubscribe/psubscribe/punsubscribe等命令实现。
1.缓存穿透
(1)概念:按照key去缓存查询value不存在时,会去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
(2)解决方式
①对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
②对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。(布隆表达式)
布隆过滤器:用于判断值可能在集合中或一定不在集合中。空间效率高和查询时间短,但删除困难和有误判率。可用于判断某邮箱是否为垃圾邮箱,或识别恶意url,还有解决缓存穿透。
原理:由长度为m的位向量组成,初始值都为0。每有一个值插入时,由k个hash函数产生k个1到m的随机散列,通过key计算多个hash方法的值,将对应位置index标记为1,检测时判断多个hash方法得到的index是否都为1–可能在,若有一个为0–一定不在。m越大则误判率越低。
2.缓存雪崩(多个缓存)
(1)概念:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
(2)解决方式:
不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期(此点为补充)
跑定时任务,在缓存失效前刷进新的数据
3.缓存击穿(单个缓存):
(1)概念:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
(2)解决方式:
使用redis的setnx互斥锁先进行判断(redis.setnx()==1),这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
(1)介绍:redis 内存数据集大小上升到一定大小的时候(超过最大内存就会崩溃),就会实行数据淘汰策略。
redis淘汰策略配置:maxmemory-policy voltile-lru,支持热配置
redis 提供 6种数据淘汰策略:这里重点关注voltile-lru。
(2)LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
LRU实现:
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
① 新数据插入到链表头部;
②每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
③当链表满的时候,将链表尾部的数据丢弃。
在Java中可以使用LinkHashMap去实现LRU。
1.MESI(modified\exclusive独占的\shared\invalid)协议
----跟redis无关可以不看,这里单纯是为了记录一个知识点
(1)介绍:通过在总线上传递消息并且各个cache控制器对消息监听,来保证数据一致性。
(2)基础知识
①内存使用DRAM的动态随机存储器;缓存在CPU内部,使用SRAM静态随机存储器。
②通常cpu中设置了多级缓存。单核处理器分为两层:L1/L2,L1分为L1P和L1D分别供程序使用和数据使用,以L1-L2-主内存找数据。多核处理器,每个核包括L1P\L1D/L2,最外层多了一个共有的L3。缓存最小存储单元为缓存行,一般为64字节。当某个数据变化,某个核没来得及变化时,会产生缓存不一致问题。
(3)读写流程
①读取流程:对于exclusive/shared/modified状态时,数据一定有效,直接读取;对于invalid状态,需要从总线发送该数据的请求,如果有一个核有该数据且状态为shared/exclusive,直接给请求的核,且e改为s,若都为i,则去主内存读。
②写流程:对于e/m状态,直接修改;某个数据在某核中被修改时,其他核要将状态改为invaild;对于i,读取数据后改为m。
2.redis的缓存一致性
我们了解了多核cpu内存中如何保证缓存一致的协议MESI。那么redis如何保证一致性呢?
(1)先更新数据库,再更新缓存场景 --不推荐
如果更新数据库的线程因为网络原因延迟更新了数据,可能会导致有一部分缓存和数据库不一致场景。
(2)先更新缓存,再更新数据库场景 --不推荐
同理。不推荐
(3)先删除缓存,再更新数据库的场景 --不推荐
同理。不推荐
(4)先更新数据库,再删除缓存场景 --推荐
如果出现了网络延迟会导致短暂数据不同步,但最终数据会一致(数据库的更新在后面)。推荐。
这里跟(1)的区别是(1)中修改了两次,这里只修改了一次。
优化:
对于不过期的数据我们要在上线的时候做好数据的预热,保证缓存命中。对于存在过期的数据,因为有过期时间,只会在特定的时间段内数据不一致,下次数据过期后,可以恢复,对于实时性要求不高时,可以接受。
(5)最佳实现,数据异步同步 --推荐
使用Canal(基于数据库增量日志解析,提供增量数据订阅和消费)
canal的工作原理:canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议;MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );canal 解析 binary log 对象(原始为 byte 流)。
mysql会将操作记录在Binary log日志中,通过canal去监听数据库日志二进制文件,解析log日志,同步到redis中进行增删改操作。
canna使用可参考:https://www.cnblogs.com/duanxz/p/5062833.html