文章目录
Redis知识点学习(二)
redis之“锁”
锁是并发编程中一个核心概念,用于控制多个线程或进程对共享资源的访问,以防止数据竞争和保证数据一致性。锁的实现和应用跨越了软件和硬件的多个层面,包括应用程序中的同步机制、操作系统的调度策略,以及硬件支持的原子操作。
锁的原理和设计
锁的基本原理是互斥,即在任何时刻,只允许一个执行线程或进程访问被保护的共享资源。设计锁时需要考虑以下因素:
-
效率:获取锁和释放锁的操作应尽可能高效。
-
公平性(避免饥饿):确保等待获取锁的线程或进程按照一定的顺序获得锁。
饥饿发生在某个或某些线程无限期地等待获取锁,而其他线程却能够频繁地获得该锁并执行,从而导致等待的线程永远无法继续执行。
-
公平锁
公平锁是避免饥饿的一种常见策略,它确保获取锁的顺序按照线程请求锁的顺序来进行。这意味着如果一个线程比其他线程先请求锁,那么它也会先于其他线程获得锁。这种机制可以通过维护一个等待队列来实现,当一个线程释放锁时,锁管理器会按照队列中的顺序将锁分配给下一个等待的线程。
-
优先级反转和优先级继承
在某些系统中,线程具有不同的优先级,高优先级的线程应当比低优先级的线程更早获取资源。然而,如果一个低优先级线程持有资源而高优先级线程在等待,可能会导致优先级反转问题,即低优先级线程因为持有资源而阻塞了高优先级线程的执行。
为了解决这个问题,可以采用优先级继承或优先级天花板协议。
优先级继承是指,当高优先级线程等待由低优先级线程持有的锁时,低优先级线程临时继承高优先级线程的优先级,以减少高优先级线程的等待时间。这种方法可以有效避免饥饿和减少优先级反转带来的影响。
-
时间片轮转
在操作系统中,时间片轮转调度算法可以帮助避免饥饿。每个线程被分配一个时间片,在其时间片内运行。当时间片用完时,即使该线程未完成其任务也会被操作系统挂起,操作系统会将CPU控制权转移给下一个等待执行的线程。这确保了所有线程都有机会执行,从而避免了饥饿。
-
令牌桶和漏桶算法
在网络流量控制和操作系统中,令牌桶和漏桶算法被用于调节数据流和控制访问速率,间接帮助避免饥饿。通过这些算法,系统能够保证资源的公平分配,并且限制了资源消耗速率,确保即使在高负载下也不会导致资源分配不公。
-
-
死锁预防:设计锁的获取和释放策略,避免死锁情况的发生。
-
可重入性:允许同一个线程多次获取同一个锁,避免自我死锁。
锁的实现
软件层面
在高级编程语言中,如Java、Python等,通过标准库提供的同步原语(如互斥量、信号量等)来实现。
在软件层面,锁通常通过编程语言提供的同步原语实现,例如:
-
互斥锁(Mutex):最基本的锁类型,保证同一时间只有一个线程可以访问某个资源。
- Mutex,即互斥锁,是最基本的锁类型,用于保证同一时间只有一个线程可以访问某个资源。Mutex通过锁定和解锁操作来实现对共享资源的互斥访问。在硬件层面,互斥锁的状态通常通过原子操作来维护,比如使用CAS(Compare And Swap)指令,这是一种比较并交换的操作,它检查内存位置的值是否等于给定值,并在是的情况下将该内存位置更新为新值。
-
读写锁(RWMutex):允许多个读操作同时进行,但写操作是互斥的,适用于读多写少的场景。
- RWMutex,即读写锁,是一种更复杂的锁,允许多个读操作同时进行,但写操作是互斥的。这意味着当一个写锁被持有时,其他线程不能读取也不能写入;当一个读锁被持有时,其他线程可以读取但不能写入。在硬件层面,读写锁的实现可能依赖于更复杂的同步原语,如读者-写者问题的解决方案,可能会涉及到对共享变量进行计数和条件变量的使用。
-
Redis分布式锁
- Redis分布式锁不是由传统的操作系统同步机制实现的,而是建立在Redis这种分布式系统之上的。利用Redis的单线程特性和原子命令,如SETNX(Set if not exists),可以实现分布式锁的基本功能。Redis分布式锁允许在分布式系统中跨多个节点同步资源访问。为了避免死锁,通常还会设置一个超时时间。
-
条件变量:允许线程在某种条件下暂停执行,并在条件满足时被唤醒。
硬件层面
利用处理器提供的原子操作指令(如CAS)来实现锁。
在硬件层面,锁状态通常通过特定的内存地址来表示。CPU提供的原子操作指令能够保证对这些地址的读写操作是不可分割的,从而避免了并发执行带来的数据竞争问题。例如,在实现互斥锁时,一个变量可能被用来表示锁的状态:0表示未锁定,1表示已锁定。通过原子性地检查和修改这个变量的值,系统能够确保在任何时刻只有一个线程能够获得锁。
在硬件层面,锁的实现依赖于CPU提供的原子操作指令,例如:
- 比较并交换(Compare and Swap, CAS):这是一种检查内存位置的值是否等于给定值,并在是的情况下将该内存位置更新为新值的原子操作。
- 测试并设置(Test and Set):设置目标内存位置为真,并返回其旧值,这一操作是原子性的。
锁在系统中的应用
操作系统通过内核提供的系统调用来实现锁机制,如POSIX线程(pthread)库中的互斥锁。
锁在操作系统、数据库管理系统、分布式系统等多个层面有广泛应用:
- 操作系统:通过内核级别的锁机制(如**自旋锁、**信号量)来管理对硬件资源和内核数据结构的访问。
- 数据库系统:利用锁来控制对数据库中数据的并发访问,保证事务的ACID属性。
- 分布式系统:分布式锁(如基于Redis或ZooKeeper实现的锁)用于控制分布在不同节点上的进程或线程对共享资源的访问。
锁的状态与硬件记录
在硬件层面,锁状态的记录和管理依赖于处理器架构提供的原子操作。这些操作能够直接影响内存中的值,而不会被其他线程或处理器核心中断。处理器通常通过**特定的指令集(如x86架构中的CMPXCHG指令)**来实现这些原子操作。此外,现代处理器还支持“锁”前缀,强制指令以原子方式执行,确保在多核环境下对共享数据的安全访问。
Redis 的使用
Redis 是一个开源的内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理。它支持多种类型的数据结构,如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)以及范围查询、位图、流和地理空间索引半径查询。使用 Redis 的基本步骤包括安装、配置、启动服务以及通过客户端与 Redis 服务器进行交互。
-
安装:可以通过官方网站下载 Redis 或者使用包管理工具如 apt 在 Ubuntu 上或 brew 在 macOS 上进行安装。
-
配置:Redis 的配置文件位于安装目录下的 redis.conf。可以根据需要修改配置文件。
-
启动服务:通过命令行使用 redis-server 命令启动 Redis 服务。可以指定配置文件作为参数。
-
客户端连接:使用 redis-cli 工具或其他编程语言的 Redis 客户端库与 Redis 服务器进行交互。
Redis 的订阅和发布
Redis 的发布/订阅(pub/sub)是一种消息通信模式:发送者(发布者)发送消息,而接收者(订阅者)接收消息。发布/订阅模式完全是无状态的,发布者和订阅者之间并不直接通信,而是通过频道进行。
- 发布:可以使用 PUBLISH 命令发送消息到一个频道。例如,PUBLISH myChannel “Hello, World!” 将消息 “Hello, World!” 发布到 myChannel 频道。
- 订阅:使用 SUBSCRIBE 命令订阅一个或多个频道。例如,SUBSCRIBE myChannel 将订阅 myChannel 频道,任何发送到该频道的消息都会被发送到订阅了该频道的客户端。
- 模式订阅:除了订阅特定的频道,客户端还可以订阅一组符合给定模式的频道。使用 PSUBSCRIBE 命令实现,如 PSUBSCRIBE my* 将订阅所有以 “my” 开头的频道。
Redis 的发布/订阅模式适用于构建高度解耦的应用程序组件、消息通知系统或实时消息传递系统等场景。
Redis 常用的数据类型及其用途(补充)
-
字符串(String):
- 底层实现:简单动态字符串(SDS,Simple Dynamic String)。
- 特点:可以包含任何数据,如文本、二进制数据等。SDS支持修改操作,如追加、截断,且能高效地进行长度计算和扩容。
- 用途:最简单的数据类型,可以存储文本或二进制数据,如缓存用户信息、会话(Session)等。
-
哈希(Hash):
- 底层实现:压缩列表(ziplist)或哈希表。
- 特点:适用于存储对象及其属性和值。当哈希元素较少且值较小时,使用ziplist以节省空间;元素增多时,转为哈希表以提高访问效率。
- 用途:存储对象及其属性和值,适合存储和访问对象类型的数据,如用户的各种属性信息。
-
列表(List):
-
底层实现:quickList。
-
特点:quickList是ziplist和双向链表的结合体。它将多个ziplist以双向链表形式串联起来,既保持了ziplist的内存效率,又提高了大量元素下的操作效率
-
用途:存储序列数据,支持在序列的两端推入或弹出元素,适合实现队列、栈等数据结构。
-
[!TIP]
关于List的底层实现优化
Redis中的List类型在Redis 7.0之前主要通过quickList实现,以解决传统双向链表在存储小元素时效率低下的问题。quickList通过减少节点数量和降低内存碎片来提高存储效率。每个quickList节点可以指向一个ziplist,该ziplist是一块连续的内存空间,能够存储多个List元素。这种设计使得在节点数量较少时,prev和next指针占用的空间比例大大减少,有效提高了数据的有效负载比例。尽管ziplist提高了空间利用率,但它仍然面临着查找效率不高的问题,因为ziplist的查找需要从头开始扫描。不过,由于ziplist是连续存储的,这种顺序扫描的速度相比于链式结构会有所提高,特别是在数据量较小的情况下。
-
-
集合(Set):
- 底层实现:整数集合(intset)或哈希表。
- 特点:适用于存储一组不重复的值。当集合中只有整数值且数量较少时,使用intset;否则使用哈希表。
- 用途:存储无序且唯一的元素集合,适合进行集合运算,如交集、并集、差集等,常用于实现标签系统、好友关系等。
-
有序集合(Sorted Set):
- 底层实现:跳跃表(skiplist)和哈希表的组合。
- 特点:每个元素都与一个分数相关联,根据分数进行排序。适合实现排行榜等功能。
- 用途:与集合类似,但每个元素都会关联一个分数,根据分数自动进行排序。适用于排行榜、带权重的集合元素管理等场景。
-
位图(Bitmap):
- 用途:通过位来表示某些状态,如在线状态、特征标志等,占用空间小,效率高。
-
HyperLogLog:
- 用途:提供不精确的去重计数功能,适用于大规模数据的计数操作,如统计网站访问量等。
-
地理空间(Geo):
- 用途:存储地理位置信息,并进行地理位置查询,如查询附近的人或地点。
基于 Redis 实现限流
要求:每分钟限定用户只能访问100次。
使用 Sorted Set 实现这个需求的步骤如下:
-
键设计:可以为每个用户维护一个 Sorted Set,键名可以是用户的唯一标识符,如用户ID拼接上当前时间的分钟数。
-
记录访问:每当用户发起请求时,使用当前时间戳作为分数,将一个唯一标识(如UUID或请求ID)加入到该用户的 Sorted Set 中。
-
清理过期数据:为了维护每分钟的访问次数限制,需要移除60秒之前的记录。可以使用 ZREMRANGEBYSCORE 命令来移除分数(时间戳)小于“当前时间戳-60秒”的所有元素。
-
计数并判断:使用 ZCARD 命令获取 Sorted Set 的元素数量,即为该分钟内用户的访问次数。如果这个数大于100,则拒绝当前请求;否则,允许访问。
# 记录访问
ZADD user:123:access 1598888888 request_id
# 清理60秒之前的数据
ZREMRANGEBYSCORE user:123:access 0 (1598888888-60)
# 获取当前分钟内的访问次数
ZCARD user:123:access
微博热搜功能如何实现? —redis应用
- 使用Redis存储搜索关键字
- 关键字计数:每当有用户进行搜索时,使用Redis的哈希表(Hashes)存储每个关键字及其搜索次数。可以使用HINCRBY命令对应关键字的计数加1。
- 有序集合存储关键字:同时,将搜索关键字作为成员加入到Redis的有序集合(Sorted Set)中,使用搜索次数作为分数(Score)。这允许我们利用Redis内置的排序功能来管理热搜排名。
- 使用小顶堆维护热搜排行
- 小顶堆概念:小顶堆是一种特殊的完全二叉树,树中每个节点的值都不大于其子节点的值。这意味着树顶(即堆顶)是所有元素中最小的。
- 限定热搜数量:通过小顶堆我们可以限定热搜列表的大小,例如只保留排名前N的热搜关键字。当新的关键字需要加入时,如果它的搜索次数高于堆顶元素,则将堆顶元素移除,新关键字加入堆中,并重新调整为小顶堆。
- 结合使用小顶堆和Redis
- 定期更新:可以设置一个定时任务,定期(例如每分钟)从Redis的有序集合中获取搜索次数最高的N个关键字,使用这些关键字构建小顶堆。
- 实时性与效率:通过这种方式,我们能够保证热搜列表既具有实时性,又不会因为频繁地从大量数据中筛选导致效率低下。
实现步骤简化
- 收集数据:用户每次搜索后,更新Redis中对应关键字的搜索次数和有序集合。
- 定期处理:通过定时任务从Redis获取当前搜索次数最高的N个关键字。
- 构建小顶堆:使用这N个关键字构建小顶堆,以维护一个大小固定的、按搜索次数排序的热搜列表。
- 提供访问:当需要展示热搜列表时,直接从小顶堆中获取。
redis应用对比
MySQL
特点:关系型数据库,提供事务支持、强一致性、ACID特性。
适用场景:
-
结构化数据存储:需要存储结构化数据,如用户信息、订单信息等,并且这些数据之间存在关联。
-
事务性应用:对事务的完整性、一致性有较高要求的应用,比如银行、电商等领域。
-
复杂查询:需要执行复杂SQL查询,包括多表联合、分组、排序等操作。
Redis
特点:内存数据结构存储,支持多种数据结构如字符串、列表、集合等,提供高性能的读写。
适用场景:
- 缓存:减轻后端数据库压力,提高数据读取速度。
- 会话存储:如用户登录会话信息。
- 消息队列:使用列表或发布/订阅模式实现消息队列。
- 实时计数器:如页面浏览量、在线用户数等。
MongoDB
特点:文档型NoSQL数据库,支持丰富的数据表达、灵活的查询语言。
适用场景:
-
文档或JSON数据存储:适合存储文档形式或JSON数据,如博客文章、配置信息等。
-
大数据量、高写入负载应用:如日志收集、社交网络信息流。
-
快速开发:灵活的数据模型使得迭代开发更加快捷。
Elasticsearch (ES)
特点:基于Lucene的搜索引擎,支持复杂的搜索查询。
适用场景:
- 全文搜索:如网站搜索、日志数据分析等。
- 数据分析:提供聚合功能,适合做实时数据分析。
- 日志和事务数据的存储与分析:结合Logstash和Kibana使用,常用于ELK(Elasticsearch, Logstash, Kibana)堆栈中。
总结:使用MySQL存储核心业务数据,Redis做缓存和会话存储,MongoDB存储日志和非结构化数据,Elasticsearch提供强大的搜索功能。
渐进式rehash
渐进式Rehash是Redis在扩展或收缩哈希表(例如,用于保存键空间或哈希数据类型的内部实现)时使用的一种技术。这个过程主要出现在Redis的字典结构中,字典是Redis处理哈希表时的基础数据结构。当哈希表中的**元素太多(负载因子过高)或太少(负载因子过低)**时,为了保持操作的效率,需要对哈希表的大小进行调整。直接重新分配一个大哈希表并一次性迁移所有元素会导致服务在迁移期间停顿,特别是当数据集很大时。,redis执行命令时为单线程模型,对当前哈希表进行rehash时,一次rehash操作可能导致服务器出现阻塞并停止对外服务。
为了避免这种情况,Redis采用了渐进式Rehash。
渐进式Rehash流程
-
分配新哈希表:当需要扩展或收缩哈希表时,Redis并不是直接对原有哈希表进行操作。相反,它保留原哈希表,并分配一个新的哈希表,新哈希表的大小更适合当前数据。
-
分步迁移:Redis不会立即将所有键值对从旧表迁移到新表,而是将迁移操作分散到后续的操作中去。每次对字典的增删改查操作时,Redis都会从旧哈希表中迁移一小部分元素到新哈希表。
-
逐步释放旧表空间:随着操作的进行,旧哈希表中的元素会逐渐减少。当所有元素都迁移到新表后,旧的哈希表就会被释放。
-
补充
在redis中,字典数据结构底层使用两个全局哈希表实现,为方便理解称之为哈希表1和哈希表2。另外,字典还维护一个rehashIdx字段,来记录rehash操作的下标位置,初始值为-1,代表未进行rehash操作
渐进式rehash的详细步骤:
- 首先为哈希表2分配空间,默认长度时哈希表1长度的2倍
- 设置索引计数器变量rehashInx为0, 表示rehash工作正式开始
- 在rehash进行期间,每次对字典执行增删改查操作时,除了执行指定的操作以外,还会把哈希表1在当前rehashInx索引上的所有键值对rehash到哈希表2。 当rehash操作完成之后 rehashInx属性的值就会+1
- 随着字典增删改查操作的不断执行,最终所有键值对都会被rehash到哈希表2中。rehash操作完成之后,哈希表1table数组的指针指向哈希表2 table数组的指针, 哈希表2 table数组的指针指向 null, 并将 rehashIdx 属性的值设置为-1, rehash操作完成。
-
[!NOTE]
- 在渐进式rehash进行期间,字典的删除,查找,更新等操作,会在两个哈希表上运行
- 在渐进式rehash执行期间,字典的添加操作权在 哈希表2 上进行
- rehash后新的下标为N或者N+size,N+为原下标,size为原数组大小
-
优势
- 避免长时间停顿:通过将Rehash操作分散到多个操作中去,Redis避免了因一次性迁移大量数据而导致的长时间停顿。
- 提高数据操作效率:通过调整哈希表大小,保持合理的负载因子,从而提高数据操作的效率。
应用场景
- 自动Rehash:在正常的操作过程中,当Redis检测到负载因子超过阈值时,会自动进行渐进式Rehash。
- 手动Rehash:管理员也可以通过执行特定命令(如BGREWRITEAOF)触发Rehash过程,以优化数据结构。
渐进式Rehash是Redis处理大规模数据动态变化时的关键技术之一,它确保了即使在数据量剧增或减少时,数据库的性能也能保持稳定。
redis特殊操作
redis中有大量的key都是长期有效的, 现需要扫描所有的key, 进行特殊判断, 符合条件进行删除,redis的删除命令-del
在Redis中处理大量长期有效的键时,如果需要扫描所有键进行特殊判断,并根据条件删除符合条件的键,可以采用以下策略:
使用SCAN命令扫描键
SCAN命令用于迭代当前数据库中的数据库键。与KEYS命令不同,SCAN命令是以渐进式的方式进行键的遍历,不会阻塞服务器。SCAN命令每次调用返回一个新的游标,直到返回的游标为0,表示遍历完成。
基本用法:
SCAN cursor [MATCH pattern] [COUNT count]
cursor:迭代的游标。
MATCH pattern:可选参数,用于指定匹配的模式。
COUNT count:可选参数,每次返回的元素数量,只是一个提示,返回数量可能会有所不同。
使用DEL命令删除键
当通过SCAN找到符合条件的键后,可以使用DEL命令进行删除。
基本用法:
DEL key [key ...]
key:要删除的键。
实践策略
结合SCAN与DEL命令,可以编写一个脚本或使用Redis客户端循环执行以下步骤:
使用SCAN命令扫描出一批键。
- 对每个键进行特殊判断,判断是否符合删除条件。
- 对符合条件的键使用DEL命令进行删除。
- 重复上述步骤直到SCAN命令返回的游标为0,表示所有键都已遍历完毕。
注意事项
执行此类操作时,应考虑对Redis服务的影响,尽量在低峰时段进行。
**使用DEL命令删除键**
当通过SCAN找到符合条件的键后,可以使用DEL命令进行删除。
**基本用法:**
```sql
DEL key [key ...]
key:要删除的键。
实践策略
结合SCAN与DEL命令,可以编写一个脚本或使用Redis客户端循环执行以下步骤:
使用SCAN命令扫描出一批键。
- 对每个键进行特殊判断,判断是否符合删除条件。
- 对符合条件的键使用DEL命令进行删除。
- 重复上述步骤直到SCAN命令返回的游标为0,表示所有键都已遍历完毕。
注意事项
执行此类操作时,应考虑对Redis服务的影响,尽量在低峰时段进行。
对于大数据集,建议将COUNT参数设置得稍大一些,以减少命令调用次数,但也要注意不要设置过大,以免影响Redis服务的响应时间。