Redis面试题

概述

什么是Redis

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。

Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。

与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,读写数据的时候不会收到硬盘I/O速度的限制,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

Redis有哪些优缺点

优点

  • 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

为什么要用 Redis /为什么要用缓存

主要从**“高性能”“高并发”**这两点来看待这个问题。

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

image-20200809142852320

高并发:

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

image-20200809142953857

为什么要用 Redis 而不用 map/guava 做缓存?

缓存分为本地缓存分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

Redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路 I/O 复用模型,非阻塞 IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

数据类型

Redis有哪些数据类型

底层

https://segmentfault.com/a/1190000021633590

字符串

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upByEyVd-1673797895365)(…/…/…/typora/image/image-20220418215532961.png)]

列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IcvGmAK4-1673797895366)(…/…/…/typora/image/image-20220418215613586.png)]

集合对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SxaQZUnm-1673797895366)(…/…/…/typora/image/image-20220418215645536.png)]

有序集合对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rn2n7Jyg-1673797895367)(…/…/…/typora/image/image-20220418215735968.png)]

散列对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3mbzlhvY-1673797895368)(…/…/…/typora/image/image-20220418215801247.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m6V1VAAK-1673797895369)(…/…/…/typora/image/image-20220418215815929.png)]

动态字符串(SDS)

双端链表

压缩列表

快速列表

整数集合

字典

跳跃表

紧凑列表

https://www.cnblogs.com/mrhgw/p/6278619.html

https://blog.youkuaiyun.com/zh15732621679/article/details/80614091

Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分的使用要求

image-20200809143238636

2.zset操作命令

[复制代码](javascript:void(0)😉

zadd(key, score, member):向名称为key的zset中添加元素member,score用于排序。如果该元素已经存在,则根据score更新该元素的顺序。
zrem(key, member) :删除名称为key的zset中的元素member
zincrby(key, increment, member) :如果在名称为key的zset中已经存在元素member,则该元素的score增加increment;否则向集合中添加该元素,其score的值为increment
zrank(key, member) :返回名称为key的zset(元素已按score从小到大排序)中member元素的rank(即index,从0开始),若没有member元素,返回“nil”
zrevrank(key, member) :返回名称为key的zset(元素已按score从大到小排序)中member元素的rank(即index,从0开始),若没有member元素,返回“nil”
zrange(key, start, end):返回名称为key的zset(元素已按score从小到大排序)中的index从start到end的所有元素
zrevrange(key, start, end):返回名称为key的zset(元素已按score从大到小排序)中的index从start到end的所有元素
zrangebyscore(key, min, max):返回名称为key的zset中score >= min且score <= max的所有元素 zcard(key):返回名称为key的zset的基数
zscore(key, element):返回名称为key的zset中元素element的score zremrangebyrank(key, min, max):删除名称为key的zset中rank >= min且rank <= max的所有元素 zremrangebyscore(key, m
还有就是执行文件cat redis-command.txt | redis-cli -h localhost -p 6379 -a 123456)

Redis的应用场景

图片说明

总结一

计数器

可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

缓存

将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

会话缓存

可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

全页缓存(FPC)

除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

查找表

例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

消息队列(发布/订阅功能)

List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。

分布式锁实现

在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

其它

Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。

总结二

Redis相比其他缓存,有一个非常大的优势,就是支持多种数据类型。

数据类型说明string字符串,最简单的k-v存储hashhash格式,value为field和value,适合ID-Detail这样的场景。list简单的list,顺序列表,支持首位或者末尾插入数据set无序list,查找速度快,适合交集、并集、差集处理sorted set有序的set

其实,通过上面的数据类型的特性,基本就能想到合适的应用场景了。

string——适合最简单的k-v存储,类似于memcached的存储结构,短信验证码,配置信息等,就用这种类型来存储。

hash——一般key为ID或者唯一标示,value对应的就是详情了。如商品详情,个人信息详情,新闻详情等。

list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的,适合根据写入的时间来排序,如:最新的***,消息队列等。

set——可以简单的理解为ID-List的模式,如微博中一个人有哪些好友,set最牛的地方在于,可以对两个set提供交集、并集、差集操作。例如:查找两个人共同的好友等。

Sorted Set——是set的增强版本,增加了一个score参数,自动会根据score的值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据。

String(字符串)

字符串是最基本的一种Redis值。Redis字符串是二进制安全的,这意味着Redis字符串可以包含任何类型的数据,例如JPEG图像或序列化的Ruby对象。字符串的长度可以是最大512MB。

可以用Redis中的字符串做一些有趣的事情,例如:

  • 将字符串用作原子计数器,使用INCR系列中的命令:INCR、DECR、INCRBY。
  • 使用Append命令向字符串追加。
  • 使用字符串作为带有GETRANGE和SETRANGE的随机访问向量。
  • 在小空间中编码大量数据,或者使用GETBIT和SETBIT创建Redis支持的Bloom过滤器。

List(字符串列表)

Redis列表是简单的字符串列表,按插入顺序排序。可以将元素添加到Redis列表中,在列表的头部(左侧)或尾部(右侧)添加新元素。LPUSH命令在头部插入一个新元素,而RPUSH在尾部插入一个新元素。当对空键执行其中一个操作时,将创建一个新列表。类似地,如果列表操作将清空列表,则从键空间中删除该键。这些都是非常方便的语义,因为如果使用不存在的键作为参数进行调用,那么所有列表命令的行为都与使用空列表调用它们时的行为完全相同。

列表的最大长度是2^32 - 1个元素(4294967295,每个列表可以包含超过40亿个元素)。从时间复杂性的角度来看,Redis列表的主要特性是支持在头和尾附近不断地插入和删除元素,即使插入了数百万元素。访问元素在列表的极端位置非常快,但是如果你试图访问一个非常大的列表的中间位置,则速度会很慢,因为这是一个O(N)操作。

可以用Redis中的list做一些有趣的事情,例如:

  • 为社交网络中的时间轴建模,使用LPUSH在用户时间线上添加新元素,使用LRANGE检索最近插入的一些项。
  • 可以使用LPUSH和LTRIM一起创建一个列表,该列表的元素数量不会超过给定的元素数量,但是只记住最新的N个元素。
  • 列表可以用作消息传递原语,例如用于创建后台作业的著名Resque Ruby库。
  • 可以使用列表做更多的事情,这种数据类型支持许多命令,包括像BLPOP这样的阻塞命令。

如上所述,虽然Redis不像关系数据库那么复杂的数据结构,但是,也能适合很多场景,比一般的缓存数据结构要多。了解每种数据结构适合的业务场景,不仅有利于提升开发效率,也能有效利用Redis的性能。

Set(集合)

Redis集合是字符串的无序集合。可以在O(1)中添加、删除和测试成员的存在性(无论集合中包含多少元素,时间都是常量)。Redis集具有不允许重复成员的理想属性。多次添加相同的元素将导致一个集合具有此元素的单一副本。实际上,这意味着添加成员不需要检查是否存在,然后添加操作。关于Redis集合,它们支持从现有集合开始计算集合的许多服务器端命令,因此可以在很短的时间内完成集合的并集、交集和差异。一个集合的最大成员数是2^32 - 1(4294967295,每个集合的成员数超过40亿)。

可以用Redis集合做很多有趣的事情,例如:

  • 可以使用Redis集跟踪唯一的东西。想知道访问给定博客文章的所有唯一IP地址吗?每次处理页面视图时只需使用SADD即可。不用担心重复ip问题,因为集合会自动过滤。
  • Redis集合很好地表示关系。可以使用Redis创建一个标记系统,使用一个集合来表示每个标记。然后,可以使用SADD命令将具有给定标记的所有对象的所有id添加到表示该特定标记的集合中。也可以使用SINTER命令返回交集成员集合。
  • 可以使用集合使用SPOP或SRANDMEMBER命令随机提取元素。

Hash(哈希)

Redis散列是字符串字段和字符串值之间的映射,因此它们是表示对象的最佳数据类型(例如,具有多个字段(如name、姓氏、年龄等)的用户):

@cli
HMSET user:1000 username antirez password P1pp0 age 34
HGETALL user:1000
HSET user:1000 password 12345
HGETALL user:1000
  • 带有几个字段的散列(其中几个字段的意思是最多100个左右)以一种占用很少空间的方式存储,因此可以在一个小型Redis实例中存储数百万个对象。
  • 虽然散列主要用于表示对象,但它们能够存储许多元素,因此你也可以将散列用于许多其他任务。
  • 每个散列最多可以存储2^32 1个字段值对(超过40亿)。

Sorted-set(有序的set集合)

与Redis集合类似,Redis排序集是字符串的非重复集合。不同之处在于,排序集的每个成员都与分数相关,分数是用来对排序集进行排序的,从最小的分数到最大的分数。虽然成员是唯一的,分数可以重复。使用排序的集合,可以以一种非常快的方式(在与元素数量的对数成比例的时间内)添加、删除或更新元素。可以非常快速地按分数或按级别(位置)获取范围。访问排序集的中间也非常快,所以可以使用排序集作为一个非重复元素的智能列表,在其中你可以快速访问所需的一切:元素顺序,快速存在测试,快速访问中间元素!简而言之,使用排序集,你可以完成许多性能优异的任务,这些任务在其他类型的数据库中很难建模。

有了排序集,你可以:

  • 在一款大型在线游戏中,你可以选择一个排行榜,每当有新的分数被提交时,你就可以使用ZADD进行更新。可以轻松地使用ZRANGE获取顶级用户,还可以在给定用户名的情况下,使用ZRANK返回其在列表中的排名。同时使用ZRANK和ZRANGE,可以向用户显示与给定用户类似的分数。这些操作都非常快。
  • 排序集通常用于索引存储在Redis中的数据。例如,如果有许多表示用户的散列,那么可以使用一个已排序的集合,其中的元素以用户的年龄为得分,以用户的ID为值。因此,使用ZRANGEBYSCORE检索给定时间间隔的所有用户既简单又快速。
  • 排序集可能是最先进的Redis数据类型,所以花点时间检查排序集命令的完整列表,以发现可以使用Redis做什么!

持久化

https://baijiahao.baidu.com/s?id=1654694618189745916&wfr=spider&for=pc

https://www.jianshu.com/p/c2610f20cec2

什么是Redis持久化?

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 的持久化机制是什么?各自的优缺点?

Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:

RDB:是Redis DataBase缩写快照

RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

image-20200809143836024

优点:

  • 1、只有一个文件 dump.rdb,方便持久化。
  • 2、容灾性好,一个文件可以保存到安全的磁盘。
  • 3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
  • 4.相对于数据集大时,比 AOF 的启动效率更高。

缺点:

  • 1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)
  • 2、AOF(Append-only file)持久化方式是指所有的命令行记录以 redis 命令请 求协议的格式完全持久化存储)保存为 aof 文件。

AOF:持久化

AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。

当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

image-20200809144054044

优点:

  • 1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
  • 2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
  • 3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点:

  • 1、AOF 文件比 RDB 文件大,且恢复速度慢。
  • 2、数据集大的时候,比 rdb 启动效率低。

优缺点是什么?

  • AOF文件比RDB更新频率高,优先使用AOF还原数据。
  • AOF比RDB更安全也更大
  • RDB性能比AOF好
  • 如果两个都配了优先加载AOF

如何选择合适的持久化方式

  • 一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
  • 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
  • 有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免AOF程序的bug。
  • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

Redis持久化数据和缓存怎么做扩容?

  • 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
  • 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。

过期键的删除策略

Redis的过期键的删除策略

我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:**只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。**极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针value是该键的毫秒精度的UNIX时间戳表示的过期时间键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期定期过期两种过期策略。

Redis key的过期时间和永久有效分别怎么设置?

EXPIRE和PERSIST命令。

我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  1. 定时去清理过期的缓存;
  2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

内存相关

MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

Redis的内存淘汰策略有哪些

img

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis主要消耗什么物理资源?

内存。

Redis的内存用完了会发生什么?

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),**散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。**比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面

线程模型

Redis线程模型

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。

事务

什么是事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

Redis事务的概念

Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务的三个阶段

  1. 事务开始 MULTI
  2. 命令入队
  3. 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

Redis事务相关命令

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的

Redis会将一个事务中的所有命令序列化,然后按顺序执行。

  1. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
  3. 如果在一个事务中出现运行错误,那么正确的命令会被执行
  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
  • UNWATCH命令可以取消watch对所有key的监控。

事务管理(ACID)概述

原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性(Consistency)
事务前后数据的完整性必须保持一致。

隔离性(Isolation)
多个事务并发执行时,一个事务的执行不应影响其他事务的执行

持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

Redis事务支持隔离性吗

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的

Redis事务保证原子性吗,支持回滚吗

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务其他实现

  • 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,
    其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
  • 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐

redis事务与mysql事务的区别

image-20200825170636581

image-20200825170802263

集群方案

哨兵模式

https://www.jianshu.com/p/06ab9daf921d

image-20200809145951830

sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

  • 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

哨兵的核心知识

  • 哨兵至少需要 3 个实例,来保证自己的健壮性。
  • 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
  • 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

image-20200809150147205

redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?

简介

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行

方案说明

  1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
  2. 每份数据分片会存储在多个互为主从的多节点上
  3. 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
  4. 同一分片多个节点间的数据不保持一致性
  5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
  6. 扩容时时需要需要把旧节点的数据迁移一部分到新节点

在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。**cluster bus 用了另外一种二进制的协议,gossip 协议,**用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

节点间的内部通信机制

基本通信原理

集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。

分布式寻址算法

  • hash 算法(大量缓存重建)
  • 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  • redis cluster 的 hash slot 算法

优点

  • 无中心架构,支持动态扩容,对业务透明
  • 具备Sentinel的监控和自动Failover(故障转移)能力
  • 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • 高性能,客户端直连redis服务,免去了proxy代理的损耗

缺点

  • 运维也很复杂,数据迁移需要人工干预
  • 只能使用0号数据库
  • 不支持批量操作(pipeline管道操作)
  • 分布式逻辑和存储模块耦合等

基于客户端分配

image-20200809150624844

简介

Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool

优点

优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强

缺点

  • 由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。
  • **客户端sharding不支持动态增删节点。**服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化

基于代理服务器分片

image-20200809150727554

简介

客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端

特征

  • 透明接入,业务程序不用关心后端Redis实例,切换成本低
  • Proxy 的逻辑和存储的逻辑是隔离的
  • 代理层多了一次转发,性能有所损耗

业界开源方案

  • Twtter开源的Twemproxy
  • 豌豆荚开源的Codis

Redis 主从架构

单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发

image-20200809150844961

redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发

redis replication 的核心机制

  • redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
  • 一个 master node 是可以配置多个 slave node 的;
  • slave node 也可以连接其他的 slave node;
  • slave node 做复制的时候,不会 block master node 的正常工作;
  • slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
  • slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。

注意,如果采用了主从架构,那么建议必须开启 master node 的持久化,不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。

另外,master 的各种备份方案,也需要做。万一本地的所有文件丢失了,从备份中挑选一份 rdb 去恢复 master,这样才能确保启动的时候,是有数据的,即使采用了后续讲解的高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。

redis 主从复制的核心原理

当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,

同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,

接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

image-20200809151147770

过程原理

  1. 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
  2. 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
  3. 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
  4. 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
  5. 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致

缺点

所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决

Redis集群的主从复制模型是怎样的?

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品

生产环境中的 redis 是怎么部署的?

redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。

机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。

5 台机器对外提供读写,一共有 50g 内存。

因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

其实大型的公司,会有基础架构的 team 负责缓存集群的运维。

说说Redis哈希槽的概念?

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

Redis集群会有写操作丢失吗?为什么?

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

Redis集群之间是如何复制的?

异步复制

Redis集群最大节点个数是多少?

16384

Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

分区

Redis是单线程的,如何提高多核CPU的利用率?

可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。

为什么要做Redis分区?

分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

你知道有哪些Redis分区实现方案?

  • **客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。**大多数客户端已经实现了客户端分区。
  • **代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。**代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy
  • **查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。**Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

Redis分区有什么缺点?

  • **涉及多个key的操作通常不会被支持。**例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
  • 同时操作多个key,则不能使用Redis事务.
  • 分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set)
  • 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
  • **分区时动态扩容或缩容可能非常复杂。**Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

分布式问题

Redis实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。

当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

image-20200809151742038

使用SETNX完成同步锁的流程及事项如下:

使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间

释放锁,使用DEL命令将锁数据删除

如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。

分布式Redis是前期做还是后期规模上来了再做好?为什么?

既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

什么是 RedLock

Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务

缓存异常

缓存雪崩

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
Bitmap: 典型的就是哈希表
缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

布隆过滤器(推荐)

大意是不同的数据结构有不同的适用场景和优缺点,你需要仔细权衡自己的需求之后妥善适用它们,布隆过滤器就是践行这句话的代表。

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

实现原理

HashMap 的问题

讲述布隆过滤器的原理之前,我们先思考一下,通常你判断某个元素是否存在用的是什么?应该蛮多人回答 HashMap 吧,确实可以将值映射到 HashMap 的 Key,然后可以在 O(1) 的时间复杂度内返回结果,效率奇高。但是 HashMap 的实现也有缺点,**例如存储容量占比高,**考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。

还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建 HashMap 的时候,也会存在问题。

布隆过滤器数据结构

布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:

image-20200825171422887

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

image-20200825171543211

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

image-20200825171615331

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

支持删除么

传统的布隆过滤器并不支持删除操作。但是名为 Counting Bloom filter 的变种可以用来测试元素计数个数是否绝对小于某个阈值,它支持元素删除。

就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。
Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁
  3. 分布式锁,只放一个请求访问数据库,这个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案

  1. 直接写个缓存刷新页面,上线时手工操作一下;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的**最终目的是保证核心服务可用,即使是有损的。**而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据

热点数据,缓存才有价值

对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存

对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

缓存热点key

缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

常用工具

Redis支持的Java客户端都有哪些?官方推荐用哪个?

Redisson、Jedis、lettuce等等,官方推荐使用Redisson。

Redis和Redisson有什么关系?

Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。

Jedis与Redisson对比有什么优缺点?

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;**Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。**Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

其他问题

Redis与Memcached的区别

两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:

image-20200809152829762

image-20200809152841734

(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

(2) redis的速度比memcached快很多

(3) redis可以持久化其数据

如何保证缓存与数据库双写时的数据一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

image-20200809153047033

  1. Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
  2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
  3. 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
  4. 尽量避免在压力较大的主库上增加从库
  5. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
  6. 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。

Redis官方为什么不提供Windows版本?

因为目前Linux版本已经相当稳定,而且用户量很大,无需开发windows版本,反而会带来兼容性等问题。

一个字符串类型的值能存储最大容量是多少?

512M

Redis如何做大量数据插入?

Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。**keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,**服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用Redis做过异步队列吗,是如何实现的

使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

Redis如何实现延时队列

使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。

Redis回收进程如何工作的?

  1. 一个客户端运行了新的命令,添加了新的数据。
  2. Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
  3. 一个新的命令被执行,等等。
  4. 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

Redis回收使用的是什么算法?

LRU算法

服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:

(1)同步阻塞IO(Blocking IO):即传统的IO模型。

(2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。

(3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。

(4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

此文主要讲IO多路复用。

重点链接

https://blog.youkuaiyun.com/diweikang/article/details/90346020

I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。结合下图可以清晰地理解I/O多路复用。

img

select, poll, epoll 都是I/O多路复用的具体的实现。epoll性能比其他几者要好。redis中的I/O多路复用的所有功能通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。

I/O是指网络I/O

多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。

即:同一个线程内同时处理多个TCP连接。 最大优势是减少系统开销小,不必创建/维护过多的线程。

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

image-20200809155146998

多路复用指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就位,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启用线程执行(比如使用线程池)、

就是说多路IO复用起到一个监视的效果,就绪后,Redis直接执行,不需要等待

串行采用多线程+锁(Memcached)与单线程+多路IO复用的比较
串行:阻塞IO,一件事一件事的做,在做当前事时,不能做其他事,有等待

采用多线程+锁:非阻塞IO,一直重复做当前的事,没有等待

单线程+多路IO复用:做某件事情需要一定的时间,可以监视这件事,我们可以做其他的事。多路IO复用有select,poll,epoll这些模式。select监测数量能力有限。poll监测数量没有限制,但是需要一个一个核查。配epoll监测数量没有限制,也不需要一个一个核查,直接看是否有一个正确的标识。

多路I/O复用模型是利用select、poll、epoll可以同时监察多个流的I/O事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路"指的是多个网络连接,“复用指的是复用同一个线程。采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了Redis具有很高的吞吐量。

redis为什么这么快?

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

那么为什么Redis是单线程的?

https://www.cnblogs.com/jichi/p/12790478.html

我们首先要明白,上边的种种分析,都是为了营造一个Redis很快的氛围!官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦! )。

为什么内存读取比硬盘快

两种的方式不同。内存是一种半导体的存储器,是ram。内存中的数据是电,一旦断电内存中的数据就会消失。内存没有机械结构。
硬盘是一种机械结构。查找数据的时候,磁盘要运动到想应的位置。磁头读取磁盘里的数据。

哈希表查询的时间复杂度(O1)

redis有用Sorted-Set的应用场景

https://www.cnblogs.com/shan1393/p/9174053.html

例如:游戏排名、微博热点话题等使用场景。

Redis常用命令

一、redis启动:
本地启动:redis-cli
远程启动:redis-cli -h host -p port -a password

Redis 连接命令

1 AUTH password
验证密码是否正确

2 ECHO message
打印字符串

3 PING
查看服务是否运行

4 QUIT
关闭当前连接

5 SELECT index
切换到指定的数据库

二、redis keys命令

1、DEL key
DUMP key
序列化给定的key并返回序列化的值

2、EXISTS key
检查给定的key是否存在

3、EXPIRE key seconds
为key设置过期时间

4、EXPIRE key timestamp
用时间戳的方式给key设置过期时间

5、PEXPIRE key milliseconds
设置key的过期时间以毫秒计

6、KEYS pattern
查找所有符合给定模式的key

7、MOVE key db
将当前数据库的key移动到数据库db当中

8、PERSIST key
移除key的过期时间,key将持久保存

9、PTTL key
以毫秒为单位返回key的剩余过期时间

10、TTL key
以秒为单位,返回给定key的剩余生存时间

11、RANDOMKEY
从当前数据库中随机返回一个key

12、RENAME key newkey
修改key的名称

13、RENAMENX key newkey
仅当newkey不存在时,将key改名为newkey

14、TYPE key
返回key所存储的值的类型

三、reids字符串命令

1、SET key value

2、GET key

3、GETRANGE key start end
返回key中字符串值的子字符

4、GETSET key value
将给定key的值设为value,并返回key的旧值

5、GETBIT KEY OFFSET
对key所储存的字符串值,获取指定偏移量上的位

6、MGET KEY1 KEY2
获取一个或者多个给定key的值

7、SETBIT KEY OFFSET VALUE
对key所是存储的字符串值,设置或清除指定偏移量上的位

8、SETEX key seconds value
将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。

9、SETNX key value
只有在 key 不存在时设置 key 的值。

10、SETRANGE key offset value
用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。

11、STRLEN key
返回 key 所储存的字符串值的长度。

12、MSET key value [key value …]
同时设置一个或多个 key-value 对。

13、MSETNX key value [key value …]
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。

14、PSETEX key milliseconds value
这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。

15、INCR key
将 key 中储存的数字值增一。

16、INCRBY key increment
将 key 所储存的值加上给定的增量值(increment) 。

17、INCRBYFLOAT key increment
将 key 所储存的值加上给定的浮点增量值(increment) 。

18、DECR key
将 key 中储存的数字值减一。

19、DECRBY key decrement
key 所储存的值减去给定的减量值(decrement) 。

20、APPEND key value
如果 key 已经存在并且是一个字符串, APPEND 命令将 指定value 追加到改 key 原来的值(value)的末尾。

四、Redis hash 命令

1 HDEL key field1 [field2]
删除一个或多个哈希表字段

2 HEXISTS key field
查看哈希表 key 中,指定的字段是否存在。

3 HGET key field
获取存储在哈希表中指定字段的值。

4 HGETALL key
获取在哈希表中指定 key 的所有字段和值

5 HINCRBY key field increment
为哈希表 key 中的指定字段的整数值加上增量 increment 。

6 HINCRBYFLOAT key field increment
为哈希表 key 中的指定字段的浮点数值加上增量 increment 。

7 HKEYS key
获取所有哈希表中的字段

8 HLEN key
获取哈希表中字段的数量

9 HMGET key field1 [field2]
获取所有给定字段的值

10 HMSET key field1 value1 [field2 value2 ]
同时将多个 field-value (域-值)对设置到哈希表 key 中。

11 HSET key field value
将哈希表 key 中的字段 field 的值设为 value 。

12 HSETNX key field value
只有在字段 field 不存在时,设置哈希表字段的值。

13 HVALS key
获取哈希表中所有值

14 HSCAN key cursor [MATCH pattern] [COUNT count]
迭代哈希表中的键值对。

五、Redis 列表命令

1 BLPOP key1 [key2 ] timeout
移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

2 BRPOP key1 [key2 ] timeout
移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

3 BRPOPLPUSH source destination timeout
从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

4 LINDEX key index
通过索引获取列表中的元素

5 LINSERT key BEFORE|AFTER pivot value
在列表的元素前或者后插入元素

6 LLEN key
获取列表长度

7 LPOP key
移出并获取列表的第一个元素

8 LPUSH key value1 [value2]
将一个或多个值插入到列表头部

9 LPUSHX key value
将一个值插入到已存在的列表头部

10 LRANGE key start stop
获取列表指定范围内的元素

11 LREM key count value
移除列表元素

12 LSET key index value
通过索引设置列表元素的值

13 LTRIM key start stop
对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。

14 RPOP key
移除并获取列表最后一个元素

15 RPOPLPUSH source destination
移除列表的最后一个元素,并将该元素添加到另一个列表并返回

16 RPUSH key value1 [value2]
在列表中添加一个或多个值

17 RPUSHX key value
为已存在的列表添加值

六、Redis 集合命令

1 SADD key member1 [member2]
向集合添加一个或多个成员

2 SCARD key
获取集合的成员数

3 SDIFF key1 [key2]
返回给定所有集合的差集

4 SDIFFSTORE destination key1 [key2]
返回给定所有集合的差集并存储在 destination 中

5 SINTER key1 [key2]
返回给定所有集合的交集

6 SINTERSTORE destination key1 [key2]
返回给定所有集合的交集并存储在 destination 中

7 SISMEMBER key member
判断 member 元素是否是集合 key 的成员

8 SMEMBERS key
返回集合中的所有成员

9 SMOVE source destination member
将 member 元素从 source 集合移动到 destination 集合

10 SPOP key
移除并返回集合中的一个随机元素

11 SRANDMEMBER key [count]
返回集合中一个或多个随机数

12 SREM key member1 [member2]
移除集合中一个或多个成员

13 SUNION key1 [key2]
返回所有给定集合的并集

14 SUNIONSTORE destination key1 [key2]
所有给定集合的并集存储在 destination 集合中

15 SSCAN key cursor [MATCH pattern] [COUNT count]
迭代集合中的元素

七、Redis 有序集合命令

1 ZADD key score1 member1 [score2 member2]
向有序集合添加一个或多个成员,或者更新已存在成员的分数

2 ZCARD key
获取有序集合的成员数

3 ZCOUNT key min max
计算在有序集合中指定区间分数的成员数

4 ZINCRBY key increment member
有序集合中对指定成员的分数加上增量 increment

5 ZINTERSTORE destination numkeys key [key …]
计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中

6 ZLEXCOUNT key min max
在有序集合中计算指定字典区间内成员数量

7 ZRANGE key start stop [WITHSCORES]
通过索引区间返回有序集合成指定区间内的成员

8 ZRANGEBYLEX key min max [LIMIT offset count]
通过字典区间返回有序集合的成员

9 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]
通过分数返回有序集合指定区间内的成员

10 ZRANK key member
返回有序集合中指定成员的索引

11 ZREM key member [member …]
移除有序集合中的一个或多个成员

12 ZREMRANGEBYLEX key min max
移除有序集合中给定的字典区间的所有成员

13 ZREMRANGEBYRANK key start stop
移除有序集合中给定的排名区间的所有成员

14 ZREMRANGEBYSCORE key min max
移除有序集合中给定的分数区间的所有成员

15 ZREVRANGE key start stop [WITHSCORES]
返回有序集中指定区间内的成员,通过索引,分数从高到底

16 ZREVRANGEBYSCORE key max min [WITHSCORES]
返回有序集中指定分数区间内的成员,分数从高到低排序

17 ZREVRANK key member
返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序

18 ZSCORE key member
返回有序集中,成员的分数值

19 ZUNIONSTORE destination numkeys key [key …]
计算给定的一个或多个有序集的并集,并存储在新的 key 中

20 ZSCAN key cursor [MATCH pattern] [COUNT count]
迭代有序集合中的元素(包括元素成员和元素分值)

八、Redis 发布订阅命令

1 PSUBSCRIBE pattern [pattern …]
订阅一个或多个符合给定模式的频道。

2 PUBSUB subcommand [argument [argument …]]
查看订阅与发布系统状态。

3 PUBLISH channel message
将信息发送到指定的频道。

4 PUNSUBSCRIBE [pattern [pattern …]]
退订所有给定模式的频道。

5 SUBSCRIBE channel [channel …]
订阅给定的一个或多个频道的信息。

6 UNSUBSCRIBE [channel [channel …]]
指退订给定的频道。

示例:
redis 127.0.0.1:6379> SUBSCRIBE redisChat

Reading messages… (press Ctrl-C to quit)

  1. “subscribe”
  2. “redisChat”
  3. (integer) 1

现在,我们先重新开启个 redis 客户端,然后在同一个频道 redisChat 发布两次消息,订阅者就能接收到消息。
redis 127.0.0.1:6379> PUBLISH redisChat “Redis is a great caching technique”

(integer) 1

订阅者的客户端会显示如下消息

  1. “message”
  2. “redisChat”
  3. “Redis is a great caching technique”

九、Redis 事务命令

1 DISCARD
取消事务,放弃执行事务块内的所有命令。

2 EXEC
执行所有事务块内的命令。

3 MULTI
标记一个事务块的开始。

4 UNWATCH
取消 WATCH 命令对所有 key 的监视。

5 WATCH key [key …]
监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

十、Redis 脚本命令

1 EVAL script numkeys key [key …] arg [arg …]
执行 Lua 脚本。

2 EVALSHA sha1 numkeys key [key …] arg [arg …]
执行 Lua 脚本。

3 SCRIPT EXISTS script [script …]
查看指定的脚本是否已经被保存在缓存当中。

4 SCRIPT FLUSH
从脚本缓存中移除所有脚本。

5 SCRIPT KILL
杀死当前正在运行的 Lua 脚本。

6 SCRIPT LOAD script
将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本。

十一、Redis 服务器命令

1 BGREWRITEAOF
异步执行一个 AOF(AppendOnly File) 文件重写操作

2 BGSAVE
在后台异步保存当前数据库的数据到磁盘

3 CLIENT KILL [ip:port] [ID client-id]
关闭客户端连接

4 CLIENT LIST
获取连接到服务器的客户端连接列表

5 CLIENT GETNAME
获取连接的名称

6 CLIENT PAUSE timeout
在指定时间内终止运行来自客户端的命令

7 CLIENT SETNAME connection-name
设置当前连接的名称

8 CLUSTER SLOTS
获取集群节点的映射数组

9 COMMAND
获取 Redis 命令详情数组

10 COMMAND COUNT
获取 Redis 命令总数

11 COMMAND GETKEYS
获取给定命令的所有键

12 TIME
返回当前服务器时间

13 COMMAND INFO command-name [command-name …]
获取指定 Redis 命令描述的数组

14 CONFIG GET parameter
获取指定配置参数的值

15 CONFIG REWRITE
对启动 Redis 服务器时所指定的 redis.conf 配置文件进行改写

16 CONFIG SET parameter value
修改 redis 配置参数,无需重启

17 CONFIG RESETSTAT
重置 INFO 命令中的某些统计数据

18 DBSIZE
返回当前数据库的 key 的数量

19 DEBUG OBJECT key
获取 key 的调试信息

20 DEBUG SEGFAULT
让 Redis 服务崩溃

21 FLUSHALL
删除所有数据库的所有key

22 FLUSHDB
删除当前数据库的所有key

23 INFO [section]
获取 Redis 服务器的各种信息和统计数值

24 LASTSAVE
返回最近一次 Redis 成功将数据保存到磁盘上的时间,以 UNIX 时间戳格式表示

25 MONITOR
实时打印出 Redis 服务器接收到的命令,调试用

26 ROLE
返回主从实例所属的角色

27 SAVE
同步保存数据到硬盘

28 SHUTDOWN [NOSAVE] [SAVE]
异步保存数据到硬盘,并关闭服务器

29 SLAVEOF host port
将当前服务器转变为指定服务器的从属服务器(slave server)

30 SLOWLOG subcommand [argument]
管理 redis 的慢日志

31 SYNC
用于复制功能(replication)的内部命令

redis的快照

redis 本地持久化到硬盘有两种方式,一是快照(snapshotting),二是只追加文件(append-only file AOF)

快照

快照,顾名思义可以理解为拍照一样,把整个内存数据映射到硬盘中,保存一份到硬盘,因此恢复数据起来比较快,把数据映射回去即可,不像AOF,一条条的执行操作命令。产生快照的过程:

1 执行bgsave命令(此时redis会fork一个子进程,子进程负责生成硬盘文件,父进程负责继续接受命令)

2 或执行save命令(和bgsave命令不同,发送save命令后,到系统创建快照完成之前系统不会再接收新的命令,换句话说save命令会阻塞后面的命令,而bgsave不会)

3 用户在配置文件了配置了类似这样的命令
save 900 1 // 900秒内,有1条写入,则产生快照

save 300 1000 // 如果300秒内有1000次写入,则产生快照

save 60 10000 // 如果60秒内有10000次写入,则产生快照

(这3个选项都屏蔽,则rdb禁用)

4 用户发送shutdown,系统会先导save命令阻塞客户端,然后关闭服务器
5 当有主从架构时,从服务器向主服务器发送sync命令来执行复制操作时,只有主服务器当时没有进行bgsave操作,那么主服务器就会执行bgsave操作。

我们可以在redis目录下查看redis.conf配置,其中某些重要配置:

stop-writes-on-bgsave-error yes // 后台备份进程出错时,主进程停不停止写入?

rdbcompression yes // 导出的rdb文件是否压缩

Rdbchecksum yes // 导入rbd恢复时数据时,要不要检验rdb的完整性

dbfilename dump.rdb //导出来的rdb文件名

dir ./ //rdb的放置路径

快照的优势

1) 通过合理的配置,可以让Redis每隔一段时间就保存一次数据库副本,也可以很方便地将数据还原到特定的时间点。

2)RDB文件相比AOF占用的空间更小,恢复数据的速度也更快。

3)如果创建RDB文件时出现了错误,Redis不会将它用于替换原来的文件,所以出错时不会影响到之前保存的版本。

快照的缺点

1) 如果硬件、系统、Redis三者其中之一出现问题而崩溃,Redis会丢失全部数据,保留下来的数据只有上一个时间点创建的快照。如果数据对于应用程序来说非常重要,那么出现错误时的损失会非常大。

2)fork子进程占用的内存随着数据库中数据的增加而增加,耗费的时间也会越来越多

问: 在dump rdb过程中,aof如果停止同步,会不会丢失?

答: 不会,所有的操作缓存在内存的队列里, dump完成后,统一操作.

问: aof重写是指什么?

答: aof重写是指把内存中的数据,逆化成命令,写入到.aof日志里.以解决 aof日志过大的问题.

问: 如果rdb文件,和aof文件都存在,优先用谁来恢复数据?

答: aof

问: 2种是否可以同时用?

答: 可以,而且推荐这么做

一般来说,如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的 bug 。因为以上提到的种种原因, 未来我们可能会将 AOF 和 RDB 整合成单个持久化模型。 (这是一个长期计划。)

问: 恢复时rdb和aof哪个恢复的快

答: rdb快,因为其是数据的内存映射,直接载入到内存,而aof是命令,需要逐条执行

redis的set数据结构 实现集合的交、差和并操作实现

1.sdiff 2个集合比较,并返回第一个集合的差集
127.0.0.1:6379> smembers set1

  1. “two”
  2. “one”
    127.0.0.1:6379> smembers set2
  3. “ghy”
  4. “one”
    127.0.0.1:6379> sdiff set1 set2
  5. “two”

2.sdiffstore 2个集合比较,并返回第一个集合的差集保存进另一个集合中
127.0.0.1:6379> sdiffstore set3 set1 set2
(integer) 1
127.0.0.1:6379> smembers set3

  1. “two”

3.sinter 2个集合比较,并返回第一个集合的交集
127.0.0.1:6379> smembers set1

  1. “two”
  2. “one”
    127.0.0.1:6379> smembers set2
  3. “ghy”
  4. “one”
    127.0.0.1:6379> sinter set1 set2
  5. “one”

4.sinterstore 2个集合比较,并返回第一个集合的交集保存进另一个集合中
127.0.0.1:6379> smembers set1

  1. “two”
  2. “one”
    127.0.0.1:6379> smembers set2
  3. “ghy”
  4. “one”
    127.0.0.1:6379> sinterstore set5 set1 set2
    (integer) 1
    127.0.0.1:6379> smembers set5
  5. “one”

5.sunion 2个集合比较,并返回两集合的并集
127.0.0.1:6379> smembers set1

  1. “two”
  2. “one”
    127.0.0.1:6379> smembers set2
  3. “ghy”
  4. “one”
    127.0.0.1:6379> sunion set1 set2
  5. “two”
  6. “one”
  7. “ghy”

6.sunionstore 2个集合比较,并返回第一个集合的并集保存进另一个集合中
127.0.0.1:6379> smembers set1

  1. “two”
  2. “one”
    127.0.0.1:6379> smembers set2
  3. “ghy”
  4. “one”
    127.0.0.1:6379> sunionstore set9 set1 set2
    (integer) 1
    127.0.0.1:6379> smembers set9
  5. “two”
  6. “one”
  7. “ghy”

redis中的hash数据结构

https://blog.youkuaiyun.com/fd2025/article/details/80066387

select poll epoll

http://gityuan.com/2015/12/06/linux_epoll/

select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。

I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。

一、select 被实现以后,很快就暴露出了很多问题。

  • select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
  • select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。
  • select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
  • select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.

“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸气

二、于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如

  • poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
  • poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。

其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。

但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。

于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.

三、epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:

  • epoll 现在是线程安全的。
  • epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。

可是epoll 有个致命的缺点,只有linux支持。比如BSD上面对应的实现是kqueue。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eq0m0Zsn-1673797895402)(…/…/…/typora/image/image-20210326175341905.png)]

redis的ZSet底层的数据结构

https://juejin.cn/post/6844904033589657607

redis的跳跃表

https://www.cnblogs.com/binbingg/p/14521878.html

Redis的hash表的渐进式Hash

https://blog.youkuaiyun.com/qq_38262266/article/details/107727116

Redis常用技术-----使用Lua语言

https://blog.youkuaiyun.com/a_helloword/article/details/81878759

Redis主从复制的原理总结

https://www.cnblogs.com/daofaziran/p/10978628.html

和Mysql主从复制的原因一样,Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3Oj5QMq-1673797895402)(…/…/…/typora/image/image-20210425101136818.png)]

全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJTQcd9c-1673797895403)(…/…/…/typora/image/image-20210425101316272.png)]

增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

主从复制的一些特点:

1)采用异步复制;
2)一个主redis可以含有多个从redis;
3)每个从redis可以接收来自其他从redis服务器的连接;
4)主从复制对于主redis服务器来说是非阻塞的,这意味着当从服务器在进行主从复制同步过程中,主redis仍然可以处理外界的访问请求;
5)主从复制对于从redis服务器来说也是非阻塞的,这意味着,即使从redis在进行主从复制过程中也可以接受外界的查询请求,只不过这时候从redis返回的是以前老的数据,
  ``如果你不想这样,那么在启动redis时,可以在配置文件中进行设置,那么从redis在复制同步过程中来自外界的查询请求都会返回错误给客户端;(虽然说主从复制过程中
  ``对于从redis是非阻塞的,但是当从redis从主redis同步过来最新的数据后还需要将新数据加载到内存中,在加载到内存的过程中是阻塞的,在这段时间内的请求将会被阻,
  ``但是即使对于大数据集,加载到内存的时间也是比较多的);
6)主从复制提高了redis服务的扩展性,避免单个redis服务器的读写访问压力过大的问题,同时也可以给为数据备份及冗余提供一种解决方案;
7)为了编码主redis服务器写磁盘压力带来的开销,可以配置让主redis不在将数据持久化到磁盘,而是通过连接让一个配置的从redis服务器及时的将相关数据持久化到磁盘,
  ``不过这样会存在一个问题,就是主redis服务器一旦重启,因为主redis服务器数据为空,这时候通过主从同步可能导致从redis服务器上的数据也被清空;

当主服务器不进行持久化时复制的安全性

在进行主从复制设置时,强烈建议在主服务器上开启持久化,当不能这么做时,比如考虑到延迟的问题,应该将实例配置为避免自动重启。
为什么不持久化的主服务器自动重启非常危险呢?
为了更好的理解这个问题,看下面这个失败的例子,其中主服务器和从服务器中数据库都被删除了。
设置节点A为主服务器,关闭持久化,节点B和C从节点A复制数据。
这时出现了一个崩溃,但Redis具有自动重启系统,重启了进程,因为关闭了持久化,节点重启后只有一个空的数据集。
节点B和C从节点A进行复制,现在节点A是空的,所以节点B和C上的复制数据也会被删除。
当在高可用系统中使用Redis Sentinel,关闭了主服务器的持久化,并且允许自动重启,这种情况是很危险的。
比如主服务器可能在很短的时间就完成了重启,以至于Sentinel都无法检测到这次失败,那么上面说的这种失败的情况就发生了。
如果数据比较重要,并且在使用主从复制时关闭了主服务器持久化功能的场景中,都应该禁止实例自动重启。

部分重新同步

从Redis 2.8开始,如果遭遇连接断开,重新连接之后可以从中断处继续进行复制,而不必重新同步。
它的工作原理是这样:
主服务器端为复制流维护一个内存缓冲区(``in``-memory backlog)。主从服务器都维护一个复制偏移量(replication offset)和master run ``id` `,
当连接断开时,从服务器会重新连接上主服务器,然后请求继续复制,假如主从服务器的两个master run ``id``相同,并且指定的偏移量在内存缓冲
区中还有效,复制就会从上次中断的点开始继续。如果其中一个条件不满足,就会进行完全重新同步(在2.8版本之前就是直接进行完全重新同步)。
因为主运行``id``不保存在磁盘中,如果从服务器重启了的话就只能进行完全同步了。
部分重新同步这个新特性内部使用PSYNC命令,旧的实现中使用SYNC命令。Redis2.8版本可以检测出它所连接的服务器是否支持PSYNC命令,不支持的
话使用SYNC命令。

Redis的哨兵机制原理

https://www.163.com/dy/article/G6IRKNKN05313LFD.html

https://www.cnblogs.com/Eugene-Jin/p/10819601.html

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance) 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地定期检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automaticfailover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中 一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客 户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主 服务器代替失效服务器。

Sentinel的工作原理总结

1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。

2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。

3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。

4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。

5):在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 。

6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 。

7):若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。

若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

漏桶算法和令牌桶算法的区别

https://blog.youkuaiyun.com/m0_37477061/article/details/95313062?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&dist_request_id=1331978.12443.16186448015760911&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control

并不能说明令牌桶一定比漏洞好,她们使用场景不一样。

==令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。==所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。

==而漏桶算法,这是用来保护他人,也就是保护他所调用的系统。==主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。
总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证别人的系统不被打垮,用漏桶算法。

漏斗的特点是匀速,有突发请求时,即使性能允许也按固定的速率处理。

比如说水桶的大小是10,而rate是每秒2,那么在一瞬间我完全可以把水桶占满呀,后续会受到rate的限制

漏斗的 rate 限制了最大的处理速度(每秒2个),而漏斗的大小限制能排队的请求数量(最多10个请求在排队)。

同样的配置,对令牌桶来说,是桶的大小限制了最大的处理速度,而 rate 限制的是满负载时的处理速度。 如果某个时刻有 10 个令牌(前5秒都没请求),然后同时到达 10 个请求,是允许同时处理这 10 个请求的。

所以说,漏斗适合请求到达频率稳定、需要严格控制处理速率的场景,而令牌桶适合允许突发请求的情况。

多路复用跟长连接有啥区别

长连接 你不需要每一次发送http请求时重新建立起TCP连接;多路复用 你可以同时发送多个http请求而不需要等到之前的请求被回应才能发送下一个请求

为什么要有LUA

Redis命令的计算能力并不算很强大,使用Lua语言则可以在很大程度上弥补Redis的这个不足。在Redis中,执行Lua语言是原子性,也就是说Redis执行Lua的时候是不会被中断的,具备原子性,这个特性有助于Redis对并发数据一致性的支持。

redis的分布式问题(秒杀、超买超卖)

使用 setnx分布式锁 设置 key value 分布式锁

由于redis是单线程的,所以不管有多少个线程都最终会以单线程方式执行setnx

大致思路:

一个线程得到分布式锁,执行对应的业务逻辑,执行完成之后,这个线程释放对应的锁,其他的线程继续获取锁,知性逻辑即可。

问题:

假如线程执行对应的业务代码,业务代码抛出了异常,那么有可能导致对应的锁没有被释放,其他线程也得不到锁,也无法正常执行

解决方法;

对应的释放锁的逻辑写到对应的finally语句块中,业务代码写到对应try中,这样的话我们就可以保证业务代码抛出异常,锁也会被及时的释放

问题:

假如执行对应的业务代码吗,导致了redis的宕机,这样也会出现我们上述的所没有被释放,导致后续的代码无法被执行

解决方法:

设置对应的锁的失效时间,这样的话即使宕机也可以释放锁

问题:

由于对应的加锁的代码和设置失效时间的代码不是原子操作,当加锁成功之后,将要执行对应的设置失效时间的代码,突然系统宕机,这样同样会导致锁没有及时释放。

解决方法:

直接将命令作为原子

问题:

并发量不高的情况下,可以接受对应的数据丢失等问题,可以这么来使用,但是在并发特别高的场景之中,此方法就不做效

例如:

锁的失效时间为 10ms

线程1 :执行时间为15ms

线程2: 执行时间为 8ms

线程3;执行时间为 5ms

…….

线程1执行了10ms,那么证明锁已经失效了,那么线程2便可以得到锁,执行业务代码,但此时线程1并没有停止,继续执行完业务代码,线程1就会去执行释放锁的逻辑,但是要知道线程1的锁已经失效了,也就是已经释放了,当前的锁是线程2的,但是线程1却释放了,出现线程释放别的线程的锁,线程2锁释放,那么线程3也能加锁,那么线程2有可能也会释放线程3的锁,导致了锁的永久失效。

解决:

我们可以给每一个线程加锁的时候设置一个唯一的标识,证明这是我们自己的锁,在释放锁的时候在进行逻辑的判断,这样保证自己释放自己的锁。

问题:

当释放锁的时候,判断为true,证明自己加的锁,那么我们释放锁,看着合情合理,但是如果在判断完了之后阻塞一段时间,在阻塞过程中锁失效了,那么别的线程加锁,阻塞结束,释放锁,但是当前的锁是别的线程的锁,还是会导致出现线程释放别的线程的问题

解决:

将对应的判断和释放作为原子操作

我们发现:其实问题一直都在失效时间无法去设置

解决:

动态设置失效时间(锁续命)

线程正常执行业务代码逻辑,但是会开一个分线程,定时任务执行分线程,分线程检测锁的失效时间,如果还是当前线程执行锁,那么就重置对应的失效时间,主线程正常执行结束,分线程也结束。

redission 直接三行命令搞定

隔多少秒 取超时时间的1/3

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0bFEcTJW-1673797895403)(…/…/…/typora/image/image-20210419174824357.png)]

    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        // KEYS[1]: 代表lockName, ARGV[1]: internalLockLeaseTime ARGV[2]:getLockName(threadId)
        
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "if (redis.call('exists', KEYS[1]) == 0) then " + // 首先判断对应key是不是存在
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " + // 不存在设置Key,Value ARGV[2] 其实是我们的线程Id
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " + // 设置Key的超时时间
                      "return nil; " +
                  "end; " +
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  "return redis.call('pttl', KEYS[1]);",
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        public long getLockWatchdogTimeout() {
            return lockWatchdogTimeout;
        }
        private long lockWatchdogTimeout = 30 * 1000;

锁续命:

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                // reschedule itself
                renewExpiration();
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        renewExpiration();
    }
}

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return 1; " +
            "end; " +
            "return 0;",
        Collections.<Object>singletonList(getName()), 
        internalLockLeaseTime, getLockName(threadId));
}

redission加锁

问题:

集群环境中主加锁同步成功,但是异步向从服务器同步时候出现问题,那么从服务器就没有锁,其他线程就可以得到锁,也可以执行并发逻辑代码

解决

Redlock

redis集群超过半数redis节点加锁成功才算加锁成功

高并发的分布式锁实现

类似于concurrentHashMap实现分段锁

缓存数据库双写不一致的问题

例如;线程1操作完数据库之后,中间出现卡顿,并没有及时的更新缓存,在此过程线程2操作数据库并更新缓存,线程1更新缓存,那么会发生线程1更新缓存的操作直接覆盖了线程2更新缓存的操作,这样也就造成了缓存中的值和数据库中值不一致。

解决:

我们在操作数据库的时候,删除缓存,当要同步的时候我们在去加缓存,

问题:

这样依然存在不一致的问题线程1查询缓存为null查找数据库,更新缓存的时候卡顿,那么线程2操作数据库,删除了缓存,线程3再去更新缓存,也出现了数据不一致

解决:

延时双删,内存队列

延时双删:也就是在操作完数据库后,过上一段时间再去删除缓存,依然出现不一致

内存队列:对于同一个key的增删改查,放到内存队列中,挨个去执行。

解决:

加分布式锁,

读写锁优化(读读不互斥,其他都互斥):读多写少场景

缓存设置失效时间

redis中如何保证缓存数据的一致性

https://blog.youkuaiyun.com/huxiaodong1994/article/details/102906696

作者:wonion
链接:https://www.nowcoder.com/discuss/659453?source_id=discuss_experience_nctrack&channel=-1
来源:牛客网

5:redis的5种数据类型,使用过哪种数据类型

redis的数据类型一般不是直接存储到内存中,而是放到了一个dictentry结构中,里面包括key指针,value指针,next指针,next指针指向下一个dictentry结构,key指针的话指向一个SDS结构,里面包括len表示数组使用了多少字节,free表示还没使用的,char类型的数组存储数据;value指针的话指向一个redisObject结构,里面包括ptr指针指向一个SDS结构,type表示value的类型,encoding表示value的内存编码,refcount计算value被引用的次数,lru最后一次修改数据的时间,5样东西

String是我们最常用的类型,内存编码有三种int类型,embstr类型,raw类型,如果是数字并且没有超过8字节会使用int类型,如果<=39字节会使用embstr类型,如果大于39字节会使用raw类型,

然后我说了为啥是39字节,因为redisObject + sds + 39正好是64字节,那么我们的redis的内存分配器 jemolloc 就可以分配64字节了;

redis3.2之前字符串长度如果小于39的话,则采取embstr存储,否则采取raw类型存储。redis3.2之后是44字节。
为啥是39?原因:对象头占16字节,空的sdshdr占用9字节,也就是一个数据至少占用16+9=25
字节,其次操作系统使用jmalloc和tmalloc进行内存的分配,而内存分配的单位都是2的N次方,所以是2,4,8,16,32,64等字节,但是redis如果采取32的话,那么32-25=7,也太他妈少了,所以Redis采取的是64字节,所以:64-25=39
Redis3.2版本之后的sds结构发生了变化【最小的是sdshdr5,空的话占用3字
节+1个空白=4字节,16+4=20;64-20=44

List列表,有序并且是可以重复的,内存编码有 压缩链表 和 有序集合 实现的

Hash:无序,内存编码有 压缩链表哈希表 实现的

set集合:是无序并且不可以重复的,内存编码有 整数集合 和 哈希表 实现的;

zset集合:有序集合,内存编码有 压缩链表 和 跳表 实现的;

(然后的话我本来还想具体说一下 压缩链表,有序集合,哈希表,整数集合,跳表 是怎么实现的,但是面试官打断我了,可能是不想听我叨叨了,然后问我使用过那些,我说在项目中使用过String 和 set)

6:redis持久化:rdb,aof

redis持久化分为了两种方式,rdb和aof,这两个最大的区别就是rdb是把数据进行持久化,aof是把写命令进行持久化;

什么是持久化:把redis中的数据 保存到磁盘上就是持久化

为什么要持久化:因为redis是在内存中操作数据,有着断点即失的特点,所以要进行持久化

过程:

rdb默认是开启的,触发方式有两种:手动触发和自动触发,手动触发的话指的是使用save/bgsave命令,我们更加推荐使用bgsave命令,因为save会全程阻塞redis主进程,而bgsave是只会在fork子进程的时候阻塞,自动触发的话可以在redis.conf文件中进行设置,sava m n,是用一个severCron函数实现的,每秒都会检查一次redis数据库在规定的时间内,修改了一定的次数,就会进行rdb持久化;

过程如下

  • 输入bgsave命令,主进程会先进行判断,是否当前进程在执行save/bgsava/bgrewriteaof命令,bgsave进程和bgrewriteaof进程不能同时存在,因为两个子进程同时进行磁盘写的操作,会引发性能问题
  • 没有其他子进程后,redis调用fork()函数,产生一个子进程,此时主进程是被阻塞的,不能处理其他命令
  • fork()结束以后,子进程会返回”Background saving started”信息并不在阻塞主进程,可以处理其他命令
  • 子进程把内存中的数据写到一个临时的rdb文件中,当子进程写完新的rdb文件后,会把旧的rdb文件替换掉
  • 子进程发消息给主进程,表示自己已经完成

aof的话是默认关闭的,需要在redis.conf文件中开启,因为记录的是写命令,所以不需要触发,而是每次把命令写入到aof_buf缓存区中去,有三种方案,always,no,everysec,always指的是一有写命令就把数据从缓存中写入到aof文件中去,但是这样会引发redis性能问题,不推荐,no的话指的是默认30s持久化一次,但是如果中间发生了故障会导致数据丢失,不推荐,everysec指的是每秒持久化一次,相当于上面两个方案的折中,推荐使用

Epollo

http://gityuan.com/2019/01/06/linux-epoll/

你真的理解内存分配吗?

https://cloud.tencent.com/developer/article/1817519

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值