当前位置: 首页 > news >正文

Redis入门第四步:Redis发布与订阅

欢迎继续跟随《Redis新手指南:从入门到精通》专栏的步伐!在本文中,我们将深入探讨Redis的发布与订阅(Pub/Sub)模式。这是一种强大的消息传递机制,适用于各种实时通信场景,如聊天应用、实时通知和事件驱动架构。通过学习本篇文章,你将能够掌握如何使用Redis的Pub/Sub功能来构建高效的消息传递系统。

在本文中,你将会学到:

  • 为什么需要发布与订阅模式:  通过业务实际案例解释为什么需要Pub/Sub模式
  • 什么是发布与订阅模式:理解Pub/Sub的基本概念及其在分布式系统中的重要性。
  • Redis的Pub/Sub命令:详细介绍如何使用PUBLISHSUBSCRIBEUNSUBSCRIBE, 和 PSUBSCRIBE等命令。
  • 频道与模式:了解频道(channels)和模式(patterns)的区别及用法。
  • 实战案例:通过一个简单的聊天应用示例,展示如何在实际项目中使用Redis的Pub/Sub功能。
  • 最佳实践:分享一些使用Pub/Sub时的最佳实践,包括性能优化和故障处理。

无论你是初学者还是有经验的开发者,本文都将为你提供清晰易懂的指导,帮助你在项目中有效利用Redis的发布与订阅功能。让我们一起探索这一强大的消息传递机制吧!

也许有的小伙伴对这个功能比较陌生,不太清楚这个功能是干什么的. 不妨我们先举个案例

假设我们有这么一个业务场景,在网站下单支付以后,需要通知库存服务进行发货处理。

上面业务实现不难,我们只要让库存服务提供给相关的给口,下单支付之后只要调用库存服务即可。

后面如果又有新的业务,比如说积分服务,他需要获取下单支付的结果,然后增加用户的积分。

这个实现也不难,让积分服务同样提供一个接口,下单支付之后只要调用库存服务即可。

如果就两个业务需要获取下单支付的结果,那也还好,程序改造也快。可是随着业务不断的发展,越来越多的新业务说是要下单支付的结果。

这时我们会发现上面这样的系统架构存在很多问题:

第一,下单支付业务与其他业务重度耦合,每当有个新业务需要支付结果,就需要改动下单支付的业务。

第二,如果调用业务过多,会导致下单支付接口响应时间变长。另外,如果有任一下游接口响应变慢,就会同步导致下单支付接口响应也变长。

第三,如果任一下游接口失败,可能导致数据不一致的情况。比如说下图,先调用 A,成功之后再调用 B,最后再调用 C。

如果在调用 B 接口的发生异常,此时可能就导致下单支付接口返回失败,但是此时 A 接口其实已经调用成功,这就代表它内部已经处理下单支付成功的结果。

这样就会导致 A,B,C 三个下游接口,A 获取成功获取支付结果,但是 B,C 没有拿到,导致三者系统数据不一致的情况。

其实我们仔细想一下,对于下单支付业务来讲,它其实不需要关心下游调用结果,只要有某种机制通知能通知到他们就可以了。

讲到这里,这就需要引入今天需要介绍发布订阅机制

1 什么是发布订阅🍀

Redis 发布订阅(pub/sub) 是一种消息通信模式: 发送者(pub)发送消息,订阅者(sub)接收消息。Redis 的 subscribe 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端

下图展示了频道channel , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

当有新消息通过 publish 命令发送给频道 channel 时, 这个消息就会被发送给订阅它的三个客户端:

如上图所示,消息发布者只需要想指定的频道发布消息,订阅该频道的每个客户端都可以接受到到这个消息。

使用 Redis 发布订阅这种机制,对于上面业务,下单支付业务只需要向「支付结果」这个频道发送消息,其他下游业务订阅「支付结果」这个频道,就能收相应消息,然后做出业务处理即可

这样就可以解耦系统上下游之间调用关系

2 为什么要用发布订阅🍀

针对消息订阅发布功能,市面上很多大厂使用的是kafkaRabbitMQActiveMQ, RocketMQ等这几种,redis的订阅发布功能跟这三者相比,相对轻量,针对数据准确和安全性要求没有那么高可以直接使用,适用于小公司

1.双端队列

redis 的List数据类型结构提供了 blpop 、brpop 命令结合 rpush、lpush 命令可以实现消息队列机制,基于双端链表实现的发布与订阅功能

这种方式存在两个局限性:

  • 不能支持一对多的消息分发。

  • 如果生产者生成的速度远远大于消费者消费的速度,易堆积大量未消费的消息

◇ 双端队列图解如下:

解析:双端队列模式只能有一个或多个消费者轮着去消费,却不能将消息同时发给其他消费者

2. 发布/订阅模式

发布/订阅模式图解如下:

解析:redis订阅发布模式,生产者生产完消息通过频道分发消息,给订阅了该频道的所有消费

3 发布/订阅如何使用🍀

Redis有两种发布/订阅模式

1.基于频道(Channel)的发布/订阅

允许用户订阅一个或多个频道,以接收所有发送到这些频道的消息。这种模型与其它消息队列不同,因为它具有消息的持久性,即使消费者在消息发送时不在线,当它们上线时,它们仍然可以接收到所有的消息

命令命令描述
subscribe channel [cha...]订阅给定的一个或多个频道
unsubscribe channel [cha...]退订给定的频道.说明:若没有指定channel,则默认退订所有频道
publish channel message将消息发送给指定频道的channel,返回结果:接收到信息的订阅者数量,无订阅者返回0
publish channels [argument [arg]....]查看订阅与发布系统的状态.说明:返回活跃频道列表(即至少有一个订阅者的频道,订阅模式的客户端除外)

"发布/订阅" 包含2种角色:发布者和订阅者。发布者可以向指定的频道(channel)发送消息;订阅者可以订阅一个或者多个频道(channel),所有订阅此频道的订阅者都会收到此消息

  • 订阅者订阅频道 subscribe channel  [channel ...]
--------------------------客户端1(订阅者) :订阅频道 ---------------------# 订阅 “meihuashisan” 和 “csdn” 频道(如果不存在则会创建频道)
127.0.0.1:6379> subscribe meihuashisan csdn 
Reading messages... (press Ctrl-C to quit)1) "subscribe"    -- 返回值类型:表示订阅成功!
2) "meihuashisan" -- 订阅频道的名称
3) (integer) 1    -- 当前客户端已订阅频道的数量1) "subscribe"
2) "csdn"
3) (integer) 2#注意:订阅后,该客户端会一直监听消息,如果发送者有消息发给频道,这里会立刻接收到消息
  • 发布者发布消息 publish channel message
-----------------------客户端2(发布者):发布消息给频道 -------------------# 给“meihuashisan”这个频道 发送一条消息:“I am meihuashisan”
127.0.0.1:6379> publish meihuashisan "I am meihuashisan"
(integer) 1  # 接收到信息的订阅者数量,无订阅者返回0
  • 客户端2(发布者)发布消息给频道后,此时我们再来观察 客户端1(订阅者)的客户端窗口变化
--------------------------客户端1(订阅者) :订阅频道 -----------------
127.0.0.1:6379> subscribe meihuashisan csdn 
Reading messages... (press Ctrl-C to quit)1) "subscribe"    -- 返回值类型:表示订阅成功!
2) "meihuashisan" -- 订阅频道的名称
3) (integer) 1    -- 当前客户端已订阅频道的数量1) "subscribe"
2) "csdn"
3) (integer) 2--------------------变化如下:(实时接收到了该频道的发布者的消息)------------
1) "message"           -- 返回值类型:消息
2) "meihuashisan"      -- 来源(从哪个频道发过来的)
3) "I am meihuashisan" -- 消息内容

注意:如果是先发布消息,再订阅频道,不会收到订阅之前就发布到该频道的消息!

注意:进入订阅状态的客户端,不能使用除了subscribeunsubscribepsubscribepunsubscribe 这四个属于"发布/订阅"之外的命令,否则会报错!

这里的客户端指的是 jedis、lettuce的客户端,redis-cli是无法退出订阅状态的

底层实现逻辑

底层通过字典实现。pubsub_channels 是一个字典类型,保存订阅频道的信息:字典的key为订阅的频道, 字典的value是一个链表, 链表中保存了所有订阅该频道的客户端

struct redisServer { /* General */ pid_t pid; //省略百十行 // 将频道映射到已订阅客户端的列表(就是保存客户端和订阅的频道信息)dict *pubsub_channels; /* Map channels to list of subscribed clients */ 
}
  • 数据结构

比如说,在下图展示的这个 pubsub_channels 示例中, client2 、 client5 和 client1 就订阅了 channel1 , 而其他频道也分别被别的客户端所订阅:

图片

  • 订阅

当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。

举个例子,如果客户端 client10086 执行命令 SUBSCRIBE channel1 channel2 channel3 ,那么前面展示的 pubsub_channels 将变成下面这个样子:

图片

  • 发布

当调用 PUBLISH channel message 命令, 程序首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端。

比如说,对于以下这个 pubsub_channels 实例, 如果某个客户端执行命令 PUBLISH channel1 "hello moto" ,那么 client2 、 client5 和 client1 三个客户端都将接收到 "hello moto" 信息:

  • 退订

使用 UNSUBSCRIBE 命令可以退订指定的频道, 这个命令执行的是订阅的反操作:它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息, 这样被退订频道的信息就不会再发送给这个客户端。

2.基于模式(pattern)的发布/订阅
命令命令描述
psubscribe pattern1 [pattern....]订阅一个或多个符合给定模式的频道.说明:每个模式以*为匹配符;例如cn*匹配所有以cn开头的频道
punsubscribe pattern1 [pattern....]退订所有给定模式的频道.说明:pattern未指定,则订阅的所有模式都会被退订,否则只退订指定的订阅模式

如果有某个/某些模式和该频道匹配,所有订阅这个/这些频道的客户端也同样会收到信息

图解

下图展示了一个带有频道和模式的例子, 其中 com.ahead.* 频道匹配了 com.ahead.juc 频道和 com.ahead.thread 频道, 并且有不同的客户端分别订阅它们三个,如下图:

当有信息发送到com.ahead.thread 频道时, 信息除了发送给 client 4client 5 之外, 还会发送给订阅 com.ahead.* 频道模式的 client xclient y

图片

解析:反之也是,如果当有消息发送给 com.ahead.juc 频道,消息发送给订阅了 juc 频道的客户端之外,还会发送给订阅了 com.ahead.* 频道的客户端: client x 、client y

通配符中?表示1个占位符,表示任意个占位符(包括0),?表示1个以上占位符。

  • 订阅者订阅频道 psubscribe pattern [pattern ...]

    --------------------------客户端1(订阅者) :订阅频道 --------------------#  1. ------------订阅 “a?” "com.*" 2种模式频道--------------
    127.0.0.1:6379> psubscribe a? com.*
    # 进入订阅状态后处于阻塞,可以按Ctrl+C键退出订阅状态
    Reading messages... (press Ctrl-C to quit) ---------------订阅成功-------------------1) "psubscribe"  -- 返回值的类型:显示订阅成功
    2) "a?"          -- 订阅的模式
    3) (integer) 1   -- 目前已订阅的模式的数量1) "psubscribe"
    2) "com.*"
    3) (integer) 2---------------接收消息 (已订阅 “a?” "com.*" 两种模式!)-----------------# ---- 发布者第1条命令:publish ahead "hello"
    结果:没有接收到消息,匹配失败,不满足 “a?” ,“?”表示一个占位符, a后面的head有4个占位符# ---- 发布者第2条命令:  publish aa "hello" (满足 “a?”)
    1) "pmessage" -- 返回值的类型:信息
    2) "a?"       -- 信息匹配的模式:a?
    3) "aa"       -- 信息本身的目标频道:aa
    4) "hello"    -- 信息的内容:"hello"# ---- 发布者第3条命令:publish com.juc "hello2"(满足 “com.*”, *表示任意个占位符)
    1) "pmessage" -- 返回值的类型:信息
    2) "com.*"    -- 匹配模式:com.*
    3) "com.juc"  -- 实际频道:com.juc
    4) "hello2"   -- 信息:"hello2"---- 发布者第4条命令:publish com. "hello3"(满足 “com.*”, *表示任意个占位符)
    1) "pmessage" -- 返回值的类型:信息
    2) "com.*"    -- 匹配模式:com.*
    3) "com."     -- 实际频道:com.
    4) "hello3"   -- 信息:"hello3"

  • 发布者发布消息 publish channel message

    ------------------------客户端2(发布者):发布消息给频道 ------------------注意:订阅者已订阅 “a?” "com.*" 两种模式!# 1. ahead 不符合“a?”模式,?表示1个占位符
    127.0.0.1:6379> publish ahead "hello"  
    (integer) 0    -- 匹配失败,0:无订阅者# 2. aa 符合“a?”模式,?表示1个占位符
    127.0.0.1:6379> publish aa "hello"      
    (integer) 1# 3. 符合“com.*”模式,*表示任意个占位符
    127.0.0.1:6379> publish com.juc "hello2" 
    (integer) 1# 4. 符合“com.*”模式,*表示任意个占位符
    127.0.0.1:6379> publish com. "hello3" 
    (integer) 1

底层是pubsubPattern节点的链表。

  • 数据结构 redisServer.pubsub_patterns 属性是一个链表,链表中保存着所有和模式相关的信息:

struct redisServer {// ...list *pubsub_patterns;// ...
};

链表中的每个节点都包含一个 redis.h/pubsubPattern 结构:

typedef struct pubsubPattern {redisClient *client;robj *pattern;
} pubsubPattern;

client 属性保存着订阅模式的客户端,而 pattern 属性则保存着被订阅的模式。

每当调用 PSUBSCRIBE 命令订阅一个模式时, 程序就创建一个包含客户端信息和被订阅模式的 pubsubPattern 结构, 并将该结构添加到 redisServer.pubsub_patterns 链表中。

作为例子,下图展示了一个包含两个模式的 pubsub_patterns 链表, 其中 client123 和 client256 都正在订阅 tweet.shop.* 模式:

图片

  • 订阅

如果这时客户端 client10086 执行 PSUBSCRIBE broadcast.list.* , 那么 pubsub_patterns 链表将被更新成这样:

图片

通过遍历整个 pubsub_patterns 链表,程序可以检查所有正在被订阅的模式,以及订阅这些模式的客户端。

  • 发布

发送信息到模式的工作也是由 PUBLISH 命令进行的, 显然就是匹配模式获得Channels,然后再把消息发给客户端。

  • 退订

使用 PUNSUBSCRIBE 命令可以退订指定的模式, 这个命令执行的是订阅模式的反操作:程序会删除 redisServer.pubsub_patterns 链表中, 所有和被退订模式相关联的 pubsubPattern 结构, 这样客户端就不会再收到和模式相匹配的频道发来的信息

4 Redis 发布订阅实际应用🍀

Redis Sentinel 节点发现

「Redis Sentinel」 是 Redis 一套高可用方案,可以在主节点故障的时候,自动将从节点提升为主节点,从而转移故障。

今天这里我们不详细解释 「Redis Sentinel」 详细原理,主要来看下 「Redis Sentinel」 如何使用发布订阅机制。

「Redis Sentinel」  节点主要使用发布订阅机制,实现新节点的发现,以及交换主节点的之间的状态。

如下所示,每一个  「Sentinel」  节点将会定时向 _sentinel_:hello 频道发送消息,并且每个 「Sentinel」 都会订阅这个节点。

这样一旦有节点往这个频道发送消息,其他节点就可以立刻收到消息。

这样一旦有的新节点加入,它往这个频道发送消息,其他节点收到之后,判断本地列表并没有这个节点,于是就可以当做新的节点加入本地节点列表。

除此之外,每次往这个频道发送消息内容可以包含节点的状态信息,这样可以作为后面 「Sentinel」 领导者选举的依据。

以上都是对于 Redis 服务端来讲,对于客户端来讲,我们也可以用到发布订阅机制。

当  「Redis Sentinel」 进行主节点故障转移,这个过程各个阶段会通过发布订阅对外提供。

对于我们客户端来讲,比较关心切换之后的主节点,这样我们及时切换主节点的连接(旧节点此时已故障,不能再接受操作指令),

客户端可以订阅 +switch-master频道,一旦  「Redis Sentinel」  结束了对主节点的故障转移就会发布主节点的的消息。

redission 分布式锁

redission 开源框架提供一些便捷操作 Redis 的方法,其中比较出名的 redission  基于 Redis 的实现分布式锁。

今天我们来看下 Redis 的实现分布式锁中如何使用 Redis 发布订阅机制,提高加锁的性能。

首先我们来看下 redission   加锁的方法:

Redisson redisson = ....
RLock redissonLock = redisson.getLock("xxxx");
redissonLock.lock();

RLock 继承自 Java 标准的 Lock 接口,调用 lock 方法,如果当前锁已被其他客户端获取,那么当前加锁的线程将会被阻塞,直到其他客户端释放这把锁。

这里其实有个问题,当前阻塞的线程如何感知分布式锁已被释放呢?

这里其实有两种实现方法:

第一钟,定时查询分布时锁的状态,一旦查到锁已被释放(Redis 中不存在这个键值),那么就去加锁。

实现伪码如下:

while (true) {boolean result=lock();if (!result) {Thread.sleep(N);}
}

这种方式实现起来起来简单,不过缺点也比较多。

如果定时任务时间过短,将会导致查询次数过多,其实这些都是无效查询。

如果定时任务休眠时间过长,那又会导致加锁时间过长,导致加锁性能不好。

那么第二种实现方案,就是采用服务通知的机制,当分布式锁被释放之后,客户端可以收到锁释放的消息,然后第一时间再去加锁。

这个服务通知的机制我们可以使用 Redis 发布订阅模式。

当线程加锁失败之后,线程将会订阅 redisson_lock__channel_xxx(xx 代表锁的名称) 频道,使用异步线程监听消息,然后利用 Java 中 Semaphore 使当前线程进入阻塞。

一旦其他客户端进行解锁,redission  就会往这个redisson_lock__channel_xxx 发送解锁消息。

等异步线程收到消息,将会调用 Semaphore 释放信号量,从而让当前被阻塞的线程唤醒去加锁。

ps:这里只是简单描述了 redission  加锁部分原理,出于篇幅,这里就不再消息解析源码。

感兴趣的小伙伴可以自己看下 redission   加锁的源码。

通过发布订阅机制,被阻塞的线程可以及时被唤醒,减少无效的空转的查询,有效的提高的加锁的效率。

ps: 这种方式,性能确实提高,但是实现起来的复杂度也很高,这部分源码有点东西,快看晕了。

5 实时聊天应用示例🍀

场景描述

假设你正在开发一个实时聊天应用,用户可以在不同的聊天室中进行交流。每个聊天室都有多个用户,当一个用户发送消息时,其他所有在该聊天室中的用户都应该立即收到这条消息。

传统解决方案的问题
  • 轮询(Polling):客户端定期向服务器发送请求,检查是否有新消息。这种方式效率低下,且响应时间较长。
  • 长轮询(Long Polling):客户端发送请求后,服务器保持连接打开,直到有新消息或超时。虽然比普通轮询更高效,但仍存在延迟和资源浪费问题。
  • WebSocket:使用WebSocket可以实现全双工通信,但设置和维护相对复杂,并且需要处理更多的网络层细节。
使用Redis Pub/Sub的优势
  • 实时性:Redis的Pub/Sub机制可以实现实时的消息传递,确保消息几乎即时送达。
  • 简单易用:Redis的Pub/Sub命令简单直观,易于集成到现有系统中。
  • 高效率:Redis是内存数据库,消息传递速度快,适合高频次的消息传输。
  • 灵活性:可以轻松地扩展到多个聊天室和大量用户。
示例实现
  1. 初始化Redis客户端

    在服务器端和客户端分别初始化Redis客户端
  2. 创建聊天室频道

    每个聊天室对应一个Redis频道。例如,chatroom:1 表示第一个聊天室
  3. 客户端订阅频道

    客户端在进入某个聊天室时,订阅对应的频道
  4. 客户端发布消息
    当用户发送消息时,客户端将消息发布到对应的频道

  5. 服务器端管理聊天室
    服务器端可以负责管理和验证用户的订阅和发布操作,确保只有合法用户能够参与聊天

# 相关代码如下: 
---------- 聊天室 服务器 代码 如下 ------------
#! /usr/bin/python
# -*- coding: UTF-8 -*-
import redis# 初始化Redis客户端
r = redis.Redis(host='localhost', port=6379, db=0, password='kdlrdsha2019')# 订阅聊天室列表
pubsub = r.pubsub()
pubsub.subscribe('chatroom:1')for message in pubsub.listen():if message['type'] == 'message':print(f"Received message: {message['data'].decode('utf-8')}")---------- 客户端使用 代码如下 ----------------
[root@test ~]# python3
Python 3.12.0 (main, Oct 27 2023, 14:11:11) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import redis
>>> r = redis.Redis(host='localhost', port=6379, db=0, password='kdlrdsha2019')
>>> r.publish('chatroom:1', 'Hello, everyone!')
1---------- 服务端返回如下 ------------------
[root@test Redis]# python3 Redis_Pub_Sub.py
Received message: Hello, everyone!

代码示例

以下是Python示例代码,展示了如何使用Redis的Pub/Sub功能实现简单的实时聊天应用。

#! /usr/bin/python
# -*- coding: UTF-8 -*-
import redis
import threading# 初始化Redis客户端, Redis服务器有密码的话需要带password参数
r = redis.Redis(host='localhost', port=6379, db=0)def subscribe_to_chatroom(chatroom):pubsub = r.pubsub()pubsub.subscribe(chatroom)print(f"Subscribed to {chatroom}")for message in pubsub.listen():if message['type'] == 'message':print(f"Received message in {chatroom}: {message['data'].decode('utf-8')}")def send_message(chatroom, message):r.publish(chatroom, message)print(f"Message sent to {chatroom}: {message}")# 创建两个线程,模拟两个客户端订阅同一个聊天室
thread1 = threading.Thread(target=subscribe_to_chatroom, args=('chatroom:1',))
thread2 = threading.Thread(target=subscribe_to_chatroom, args=('chatroom:1',))thread1.start()
thread2.start()# 模拟发送消息
send_message('chatroom:1', 'Hello, everyone!')
send_message('chatroom:1', 'How are you?')

总结

        在这个示例中,Redis的Pub/Sub功能使得实时聊天应用变得非常简单和高效。客户端可以轻松地订阅和接收消息,而无需复杂的网络配置。这种模式不仅适用于聊天应用,还可以用于各种需要实时消息传递的场景,如实时通知、日志聚合、事件驱动架构等。

        通过这个例子,读者可以清楚地看到Redis的Pub/Sub功能在实际项目中的强大作用和实用性。希望这能帮助你更好地讲解这个功能的重要性!

相关文章:

Redis入门第四步:Redis发布与订阅

欢迎继续跟随《Redis新手指南:从入门到精通》专栏的步伐!在本文中,我们将深入探讨Redis的发布与订阅(Pub/Sub)模式。这是一种强大的消息传递机制,适用于各种实时通信场景,如聊天应用、实时通知和…...

MySQL 之权限与授权

MySQL 权限及授权系统用于控制数据库用户对数据库资源的访问和操作权限。它提供了一种细粒度的安全控制机制,确保只有被授权的用户才能执行特定的操作。MySQL 的权限控制体系非常灵活,支持多种权限类型及级别(数据库、表、列、存储过程等&…...

解决方案:Pandas里面的loc跟iloc,有什么区别

文章目录 一、现象二、解决方案案例使用loc使用iloc 简单总结 一、现象 在用Pandas库处理数据的时候,久而久之不用loc跟iloc,难免会有些混乱记混 二、解决方案 在Pandas中,loc和iloc是两种常用的数据选择方法,它们的主要区别在…...

C# 和 C++ 混合编程

以下是一个关于 C# 和 C 混合编程 的教程详细目录,涵盖了混合编程中的各个重要方面: 目录 1. 引言 1.1 什么是混合编程? 1.2 为什么选择 C# 和 C 进行混合编程? 1.3 应用场景和优势 2. 基本概念 2.1 C# 和 C 的基础差异 2.…...

Vxe UI vue vxe-table 实现表格单元格选中功能

Vxe UI vue vxe-table 实现表格单元格选中功能 在表格中实现鼠标点击任意单元格,选取的功能,通过 mouse-config 配置就可以开启单选功能,多选单元格选取功能需安装插件支持。 代码 参数说明 mouse-config 鼠标配置项: selected&…...

组合模式详解

1、组合模式基本介绍 1) 组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以 表示“整体-部分”的层次关系。 2) 组合模式依据树形结构来组合对象,用来表示部…...

AltiumDesigner脚本开发-DIP封装制作

1.点击工具栏的运行工具(蓝色向右三角图标)可以执行脚本程序; 2.点击菜单栏Run->Run可以执行脚本程序; 3.在脚本编辑器中,按键盘的F9键可以执行脚本程序; 4.通过菜单栏执行脚本程序(需要将程序添加到菜单栏中&am…...

乌班图基础设施安装之Mysql8.0+Redis6.X安装

简介:云服务器基础设施安装之 Mysql8.0Redis6.X 安装 Docker安装 # 按照依赖 yum install -y yum-utils device-mapper-persistent data lvm2 Docker Mirror 从去年开始. hub.docker.com[1] 在国内的访问速度极慢. 当时大家主要还是依赖国内的一些镜像源: 如中科…...

【动态规划-最长递增子序列(LIS)】力扣673.最长递增子序列的个数

给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。 注意 这个数列必须是 严格 递增的。 示例 1: 输入: [1,3,5,4,7] 输出: 2 解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。 示例 2: 输入: [2,2,2,2,2] 输出: 5 解释:…...

SQL优化 where谓词条件is null优化

1.创建测试表及谓词条件中包含is null模拟语句 create table t641 as select * from dba_objects; set autot trace select SUBOBJECT_NAME,OBJECT_NAME from t641 where OBJECT_NAMEWRI$_OPTSTAT_SYNOPSIS$ and SUBOBJECT_NAME is null; 2.全表扫描逻辑读1237 3.创建等值谓词条…...

Starrocks 元数据恢复 failed to load journal type 10242

fe 启动异常 2024-10-08 09:24:57.66908:00 INFO (stateChangeExecutor|87) [DatabaseTransactionMgr.replayUpsertTransactionState():1702] remove expired transaction: TransactionState. txn_id: 189324, label: delete_031c5090-7e2d-11ef-bdd8-000c29967e13, db id: 100…...

《深度学习》神经语言模型 Word2vec CBOW项目解析、npy/npz文件解析

目录 一、关于word2vec 1、什么是word2vec 2、常用训练算法 1)CBOW 2)SkipGram 二、关于npy、npz文件 1、npy文件 1)定义 2)特性 3)用途 4)保存及读取 运行结果: 运行结果&#xf…...

黄粱一梦,镜花水月总是空

总有人间一两风,埋我十万八千梦 自古以来,梦在我们的生活中一直是一个神秘玄幻而又发人深省的存在,我们一生中有三分之一的时间都在睡觉,做过的梦也是丰富多彩数不胜数。 而从科学的角度来说,梦是我们潜意识里的生活…...

【分布式事务-01】分布式事务之2pc两阶段提交

redis系列整体栏目 内容链接地址【一】分布式事务之2pc两阶段提交https://zhenghuisheng.blog.csdn.net/article/details/142406325 分布式事务之2pc两阶段提交 一,分布式事务之2pc两阶段提交1,两阶段提交(2pc)2,2pc两阶段提交实现思路3&…...

docker 安装 rabbitMQ

第一步:准备工作 # 打开docker目录 [rootMuYu ~]# cd /usr/local/docker/ # 创建rabbitmq文件夹 [rootMuYu docker]# mkdir rabbitmq # 打开rabbitmq文件夹 [rootMuYu docker]# cd rabbitmq/ # 创建挂载目录 [rootMuYu rabbitmq]# mkdir data 第二步&#xff…...

知识改变命运 数据结构【java对象的比较】

0:前言 在基本数据类型中,我们可以直接使用号比较是否相等,还记的学堆哪里时候,插入一个数据,就会与其他数据进行比较,当时我们传入的是Integer类型,在Integer类里面已经实现了compare。 如果…...

01_23 种设计模式之《简单工厂模式》

文章目录 一、什么是设计模式二、设计模式类型简单工厂模式及应用场景定义抽象产品类和具体产品类实现工厂类客户端代码注意事项 一、什么是设计模式 设计模式:在软件研发过程中,经过实战验证,用于解决在特定环境下、重复出现的,…...

Android 12.0 关于定制自适应AdaptiveIconDrawable类型的动态日历图标的功能实现系列一

1.前言 在12.0的系统rom定制化开发中,在关于定制动态日历图标中,原系统是不支持动态日历图标的功能,所以就需要从新 定制动态时钟图标关于自适应AdaptiveIconDrawable类型的样式,就是可以支持当改变系统图标样式变化时,动态日历 图标的背景图形也跟着改变,所以接下来就来…...

【源码+文档+调试讲解】基于安卓的小餐桌管理系统springboot框架

摘 要 相比于以前的传统手工管理方式,智能化的管理方式可以大幅降低运营人员成本,实现了小餐桌的标准化、制度化、程序化的管理,有效地防止了小餐桌的随意管理,提高了信息的处理速度和精确度,能够及时、准确地查询和修…...

C语言中的文件操作(二)

C语言中的文件操作&#xff08;一&#xff09;-CSDN博客https://blog.csdn.net/Xiaodao12345djs/article/details/142746010?spm1001.2014.3001.5501 四、文件的顺序读写 1、fputc (字符输出函数/写) 将一个字符写入文件中 #include <stdio.h>int main() {FILE* pf fo…...

【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则

文章目录 C 继承详解&#xff1a;初阶理解与实战应用前言第一章&#xff1a;继承的基本概念与定义1.1 继承的概念1.2 继承的定义 第二章&#xff1a;继承中的访问权限2.1 基类成员在派生类中的访问权限2.2 基类与派生类对象的赋值转换2.2.1 派生类对象赋值给基类对象2.2.2 基类…...

Ubuntu 22.04 安装 KVM

首先检查是否支持 CPU 虚拟化&#xff0c;现在的 CPU 都应该支持&#xff0c;运行下面的命令&#xff0c;大于0 就是支持。 egrep -c (vmx|svm) /proc/cpuinfo安装 Libvirt apt install -y qemu-kvm virt-manager libvirt-daemon-system virtinst libvirt-clients bridge-uti…...

101 公司战略的基本概念

公司战略的概念 传统概念&#xff08;战略是终点途径&#xff09;&#xff1a;计划性、全局性、长期性现代概念&#xff08;战略是途径&#xff09;&#xff1a;应变性、竞争性、风险性综合概念&#xff08;前二者的折中&#xff09;&#xff1a;预先性、反应性公司的使命与目标…...

【devops】devops-ansible之剧本初出茅庐--搭建rsync和nfs

本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》:python零基础入门学习 《python运维脚本》: python运维脚本实践 《shell》:shell学习 《terraform》持续更新中:terraform_Aws学习零基础入门到最佳实战 《k8》从问题中去学习k8s 《docker学习》暂未更…...

@RestController 和 @Controller 注解的联系及要点

1. RestController • RestController 是 Spring 4.0 引入的一个注解&#xff0c;它相当于 Controller ResponseBody组合注解。 主要作用&#xff1a;主要用于构建 RESTful Web 服务。标注 RestController 的类里的所有方法&#xff0c;返回的都是 JSON 或 XML 等格式的数据…...

机器学习篇-day03-线性回归-正规方程与梯度下降-模型评估-正则化解决模型拟合问题

一. 线性回归简介 定义 线性回归(Linear regression)是利用 回归方程(函数) 对 一个或多个自变量(特征值)和因变量(目标值)之间 关系进行建模的一种分析方式。 回归方程(函数) 一元线性回归: y kx b > wx b k: 斜率, 在机器学习中叫 权重(weight), 简称: w b: 截距, 在机…...

图像人脸与视频人脸匹配度检测

import cv2 import dlib import numpy as np import os from pathlib import Path# 加载预训练模型 face_recognition_model "dlib_face_recognition_resnet_model_v1.dat" face_recognition_net dlib.face_recognition_model_v1(face_recognition_model)detector …...

【AI绘画】Midjourney进阶:对称构图详解

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AI绘画 | Midjourney 文章目录 &#x1f4af;前言&#x1f4af;什么是构图为什么Midjourney要使用构图 &#x1f4af;对称构图特点使用场景提示词书写技巧测试 &#x1f4af;小结 &#x1f4af;前言 通常来学习AI绘画的人可以分为…...

道路积水检测数据集 1450张 路面积水 带分割 voc yolo

道路积水检测数据集 1450张 路面积水 带分割 voc yolo 分类名: (图片张数&#xff0c; 标注个数) puddle:(1468,1994) 总数:(1468&#xff0c;1994) 总类(nc): 1类 道路积水检测数据集介绍 项目名称 道路积水检测数据集 项目概述 本数据集包含1450张带有标注的图像&#x…...

上门安装维修系统小程序开发详解及源码示例

随着智能家居和设备的普及&#xff0c;消费者对上门安装和维修服务的需求日益增加。为了满足这一市场需求&#xff0c;开发一款上门安装维修系统小程序成为了一种有效的解决方案。本文将详细介绍上门安装维修系统小程序的开发过程&#xff0c;并提供一个简单的源码示例&#xf…...