redis实现消息队列redis发布订阅redis监听key

文章目录

  • Redis消息队列实现异步秒杀
    • 1. jvm阻塞队列问题
    • 2. 什么是消息队列
    • 3. Redis实现消息队列
      • 1. 基于List结构模拟消息队列
        • 操作
        • 优缺点
      • 2. 基于PubSub发布订阅的消息队列
        • 操作
        • 优缺点
        • spring 结合redis的pubsub使用示例
          • 1. 引入依赖
          • 2. 配置文件
          • 3. RedisConfig
          • 4. CustomizeMessageListener
          • 5. RedisMessageReceiver
          • 6. 监听原理简析
          • 7. 监听redis的key
            • 修改redis.conf
            • KeyspaceEventMessageListener
            • KeyExpirationEventMessageListener
            • 修改RedisConfig

Redis消息队列实现异步秒杀

1. jvm阻塞队列问题

java使用阻塞队列实现异步秒杀存在问题:

  1. jvm内存限制问题:jvm内存不是无限的,在高并发的情况下,当有大量的订单需要创建时,就有可能超出jvm阻塞队列的上限。
  2. 数据安全问题:jvm的内存没有持久化机制,当服务重启或宕机时,阻塞队列中的订单都会丢失。或者,当我们从阻塞队列中拿到订单任务,但是尚未处理时,如果此时发生了异常,这个订单任务就没有机会处理了,也就丢失了。

2. 什么是消息队列

消息队列Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:

  • 消息队列:存储和管理消息,也被称为消息代理(Message Broker)
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息

在这里插入图片描述

(正常下单,我们需要将订单消息写入数据库。但由于秒杀并发访问量大,数据库本身并发处理能力不强,因此,在处理秒杀业务时,可以将部分业务在生产者这边做校验,然后将消息写入消息队列,而消费者处理该消息队列中的消息,从而实现双方解耦,更快的处理秒杀业务)

3. Redis实现消息队列

我们可以使用一些现成的mq,比如kafka,rabbitmq等等,但是呢,如果没有安装mq,我们也可以直接使用redis提供的mq方案,降低我们的部署和学习成本。Redis提供了三种不同的方式来实现消息队列:

  • list结构:基于List结构模拟消息队列
  • PubSub:基本的点对点消息模型
  • Stream:比较完善的消息队列模型

1. 基于List结构模拟消息队列

消息队列(Message Queue),字面意思就是存放消息的队列。而Redis的list数据结构是一个双向链表,很容易模拟出队列效果。

队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现

不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果

在这里插入图片描述

操作

命令介绍如下

在这里插入图片描述

在这里插入图片描述

优缺点

优点:

  • 利用Redis存储,不受限于JVM内存上限
  • 基于Redis的持久化机制,数据安全性有保证
  • 可以满足消息有序性

缺点:

  • 无法避免消息丢失(如果消费者获取消息后,然后立马就宕机了,这个消息就得不到处理,等同于丢失了)
  • 只支持单消费者(1个消息只能被1个消费者取走,其它消费者会收不到此消息)

2. 基于PubSub发布订阅的消息队列

PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。

  • SUBSCRIBE channel [channel] :订阅一个或多个频道
  • PUBLISH channel msg :向一个频道发送消息
  • PSUBSCRIBE pattern [pattern] :订阅与pattern格式匹配的所有频道
    • ?匹配1个字符:h?llo subscribes to hello, hallo and hxllo
    • *匹配0个或多个字符:h*llo subscribes to hllo and heeeello
    • []指定字符:h[ae]llo subscribes to hello and hallo, but not hillo

在这里插入图片描述

操作

在这里插入图片描述

优缺点

优点:

  • 采用发布订阅模型,支持多生产、多消费

缺点:

  • 不支持数据持久化(如果发送消息时,这个消息的频道没有被任何人订阅,那这个消息就丢失了,也消息就是不会被保存)
  • 无法避免消息丢失(发完了,没人收,直接就丢了)
  • 消息堆积有上限,超出时数据丢失(当我们发送消息时,如果有消费者在监听,消费者会有1个缓存区去缓存这个消息数据,如果消费者处理的慢,那么客户端的缓存区中的消息会不断堆积,而这个缓存区是有大小限制的,如果超出了就会丢失)
spring 结合redis的pubsub使用示例
1. 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.zzhua</groupId>
    <artifactId>demo-redis-pubsub</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

		<!-- 如果使用lettuce-core作为连接redis的实现, 
		    不引入此依赖会报错: Caused by: java.lang.ClassNotFoundException:
		                                org.apache.commons.pool2.impl.GenericObjectPoolConfig -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2. 配置文件
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    password:
    lettuce:
      pool:
        min-idle: 2
        max-active: 8
        max-idle: 8
3. RedisConfig

spring-data-redis提供了2种处理redis消息的方法:

  • 自己实现MessageListener接口

    public interface MessageListener {
    	// 处理消息的方法
    	// 第1个参数封装了: 消息发布到哪1个具体频道 和 消息的内容
    	// 第2个参数封装了: 
    	//     1. 如果当前是通过普通模式去订阅的频道, 那么收到消息时该pattern就是消息发送的具体频道
    	//     2. 如果当前是通过pattern通配符匹配去订阅的频道, 那么收到消息时, 该pattern就是订阅的频道
    	void onMessage(Message message, @Nullable byte[] pattern);
    }
    
  • 指定MessageListenerAdapter适配器,该适配器指定特定对象的特定方法来处理消息(对特定的方法有参数方面的要求)

@Slf4j
@Configuration
public class RedisConfig {

    @Autowired
    private RedisMessageReceiver redisMessageReceiver;

    @Autowired
    private CustomizeMessageListener customizeMessageListener;

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();

        container.setConnectionFactory(connectionFactory);

        // 监听order.q通道(不带通配符匹配channel)
        container.addMessageListener(customizeMessageListener, new ChannelTopic("order.q"));

        // 监听order.*通道(带通配符匹配channel)
        container.addMessageListener(listenerAdapter(), new PatternTopic("order.*"));

        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter() {
        // 交给receiver的receiveMessage方法, 对于这个方法的参数有如下要求:
        // (2个参数: 第一个参数是Object-即消息内容(默认由RedisSerializer#deserialize处理,见MessageListenerAdapter#onMessage), 
        //           第二个参数是String-即订阅的通道, 详细看上面MessageListener接口中第二个参数的解释)
        // (1个参数: 参数是Object-即消息内容)
        return new MessageListenerAdapter(redisMessageReceiver, "receiveMessage");
    }

}

4. CustomizeMessageListener
@Slf4j
@Component
public class CustomizeMessageListener implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {

        byte[] bodyBytes = message.getBody();

        byte[] channelBytes = message.getChannel();

        log.info("order.q - 消息订阅频道: {}", new String(channelBytes));
        log.info("order.q - 消息内容: {}", new String(bodyBytes));
        log.info("order.q - 监听频道: {}", new String(channelBytes));

    }
}
5. RedisMessageReceiver
@Slf4j
@Component
public class RedisMessageReceiver {

    public void receiveMessage(String msg, String topic) {
        log.info("order.* - 消息的订阅频道: {}", topic);
        log.info("order.* - 消息的内容: {}", msg);
    }

}

6. 监听原理简析

spring-data-redis的lettuce-core是基于netty的,消息监听处理过程如下:
PubSubCommandHandler(netty中的ChannelHandler处理器)->PubSubEndpoint(根据消息类型调用LettuceMessageListener 的不同方法)->LettuceMessageListener -> RedisMessageListenerContainer$DispatchMessageListener(如果是pattern,则从patternMapping中获取所有的listener;如果不是pattern,则从channelMapping中获取所有的listener。至于怎么判断是不是pattern?)->使用异步线程池对上一步获取的所有listener执行onMessage方法

至于怎么判断是不是pattern?这个是根据订阅关系来的,如果订阅的是pattern,那么如果这个向这个pattern中发送了消息,那么就会收到1次消息,并且是pattern。如果订阅的是普通channel,那么如果向这个普通channel发送了消息,那么又会收到1次消息不是pattern。如果向1个channel中发送消息,这个channel既符合订阅的pattern,也符合订阅的普通channel,那么会收到2次消息,并且这2次消息1次是pattern,1次不是pattern的

7. 监听redis的key

既然已经说到了监听redis发布消息了,那么也补充一下监听redis的key过期。因为监听redis的key过期也是通过redis的发布订阅实现的。

修改redis.conf
############################# EVENT NOTIFICATION ##############################

# Redis能够将在keyspace中发生的事件通知给 发布/订阅 客户端

# Redis can notify Pub/Sub clients about events happening in the key space. 
# This feature is documented at http://redis.io/topics/notifications

# 例如:如果开启了keyspace事件通知(注意了,必须是开启了keyspace事件通知才可以,开启的方式就是添加参数K),
#      一个客户端在数据库0对一个叫'foo'的key执行了删除操作,
#      那么redis将会通过 发布订阅 机制发布2条消息 
#        PUBLISH __keyspace@0__:foo del 
#        PUBLISH __keyevent@0__:del foo

# For instance if keyspace events notification is enabled, and a client
# performs a DEL operation on key "foo" stored in the Database 0, two
# messages will be published via Pub/Sub:
#
# PUBLISH __keyspace@0__:foo del
# PUBLISH __keyevent@0__:del foo

#  也可以指定一组 类名 来选择 Redis 会通知的一类事件。
#  每类事件 都通过一个字符定义

# It is possible to select the events that Redis will notify among a set
# of classes. Every class is identified by a single character:

#        keySpace事件   以 __keyspace@<数据库序号>__ 为前缀 发布事件
#  K     Keyspace events, published with __keyspace@<db>__ prefix.  

#        Keyevent事件   以 __keyevent@<数据库序号>__ 为前缀 发布事件
#  E     Keyevent events, published with __keyevent@<db>__ prefix.        

#        执行常规命令,比如del、expire、rename           
#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...      

#        执行 String 命令
#  $     String commands  

#        执行 List   命令                                                          
#  l     List commands  

#        执行 Set    命令                                                         
#  s     Set commands  

#        执行 Hash   命令                                                         
#  h     Hash commands                                                          执行 Hash   命令

#        执行 ZSet   命令
#  z     Sorted set commands 

#        key过期事件(每个key失效都会触发这类事件)                                                   
#  x     Expired events (events generated every time a key expires) 
   
#        key驱逐事件(当key在内存满了被清除时生成)
#  e     Evicted events (events generated when a key is evicted for maxmemory)  

#        A是g$lshzxe的别名,因此AKE就意味着所有的事件
#  A     Alias for g$lshzxe, so that the "AKE" string means all the events.     
#

#  配置中的notify-keyspace-events这个参数由0个或多个字符组成,
#  如果配置为空字符串表示禁用通知
#  The "notify-keyspace-events" takes as argument a string that is composed     
#  of zero or multiple characters. The empty string means that notifications
#  are disabled.
#

#  比如,要开启list命令和generic常规命令的事件通知,
#  应该配置成 notify-keyspace-events Elg
#  Example: to enable list and generic events, from the point of view of the    
#           event name, use:
#
#  notify-keyspace-events Elg
#
#  比如,订阅了__keyevent@0__:expired频道的客户端要收到key失效的时间,
#  应该配置成 notify-keyspace-events Ex
#  Example 2: to get the stream of the expired keys subscribing to channel   name __keyevent@0__:expired use:
#
#  notify-keyspace-events Ex
#

#  默认情况下,所有的通知都被禁用了,并且这个特性有性能上的开销。
#  注意,K和E必须至少指定其中一个,否则,将收不到任何事件。
#  By default all notifications are disabled because most users don't need      
#  this feature and the feature has some overhead. Note that if you don't       
#  specify at least one of K or E, no events will be delivered.
notify-keyspace-events "Ex"

############################### ADVANCED CONFIG ###############################

KeyspaceEventMessageListener
  1. 通过实现InitializingBean接口,在afterPropertiesSet方法中,调用初始化init方法,从redis中获取notify-keyspace-events配置项对应的值,如果未设置任何值,则改为EA,结合上面的redis.conf节选可知,表示的是开启所有的事件通知
  2. 使用redisMessageListenerContainer,通过pattern通配符匹配的方式订阅__keyevent@*频道
  3. 它是个抽象类,实现了MessageListener接口,处理消息的方法是个抽象方法
  4. 它有1个子类KeyExpirationEventMessageListener,订阅的pattern的频道是:__keyevent@*__:expired,通过重写doRegister修改了订阅的频道。并且重写了处理消息的方法,通过将消息内容包装成RedisKeyExpiredEvent事件对象,然后通过事件发布器将事件发布出去。
public abstract class KeyspaceEventMessageListener implements MessageListener, InitializingBean, DisposableBean {

	private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*");

	private final RedisMessageListenerContainer listenerContainer;

	private String keyspaceNotificationsConfigParameter = "EA";

	/**
	 * Creates new {@link KeyspaceEventMessageListener}.
	 *
	 * @param listenerContainer must not be {@literal null}.
	 */
	public KeyspaceEventMessageListener(RedisMessageListenerContainer listenerContainer) {

		Assert.notNull(listenerContainer, "RedisMessageListenerContainer to run in must not be null!");
		this.listenerContainer = listenerContainer;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.connection.MessageListener#onMessage(org.springframework.data.redis.connection.Message, byte[])
	 */
	@Override
	public void onMessage(Message message, @Nullable byte[] pattern) {

		if (message == null || ObjectUtils.isEmpty(message.getChannel()) || ObjectUtils.isEmpty(message.getBody())) {
			return;
		}

		doHandleMessage(message);
	}

	/**
	 * Handle the actual message
	 *
	 * @param message never {@literal null}.
	 */
	protected abstract void doHandleMessage(Message message);

	/**
	 * Initialize the message listener by writing requried redis config for {@literal notify-keyspace-events} and
	 * registering the listener within the container.
	 */
	public void init() {

		if (StringUtils.hasText(keyspaceNotificationsConfigParameter)) {

			RedisConnection connection = listenerContainer.getConnectionFactory().getConnection();

			try {

				Properties config = connection.getConfig("notify-keyspace-events");

				if (!StringUtils.hasText(config.getProperty("notify-keyspace-events"))) {
					connection.setConfig("notify-keyspace-events", keyspaceNotificationsConfigParameter);
				}

			} finally {
				connection.close();
			}
		}

		doRegister(listenerContainer);
	}

	/**
	 * Register instance within the container.
	 *
	 * @param container never {@literal null}.
	 */
	protected void doRegister(RedisMessageListenerContainer container) {
		listenerContainer.addMessageListener(this, TOPIC_ALL_KEYEVENTS);
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.beans.factory.DisposableBean#destroy()
	 */
	@Override
	public void destroy() throws Exception {
		listenerContainer.removeMessageListener(this);
	}

	/**
	 * Set the configuration string to use for {@literal notify-keyspace-events}.
	 *
	 * @param keyspaceNotificationsConfigParameter can be {@literal null}.
	 * @since 1.8
	 */
	public void setKeyspaceNotificationsConfigParameter(String keyspaceNotificationsConfigParameter) {
		this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		init();
	}
}

KeyExpirationEventMessageListener
public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener implements
		ApplicationEventPublisherAware {

	private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired");

	private @Nullable ApplicationEventPublisher publisher;

	/**
	 * Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages.
	 *
	 * @param listenerContainer must not be {@literal null}.
	 */
	public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) {
		super(listenerContainer);
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#doRegister(org.springframework.data.redis.listener.RedisMessageListenerContainer)
	 */
	@Override
	protected void doRegister(RedisMessageListenerContainer listenerContainer) {
		listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC);
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#doHandleMessage(org.springframework.data.redis.connection.Message)
	 */
	@Override
	protected void doHandleMessage(Message message) {
		publishEvent(new RedisKeyExpiredEvent(message.getBody()));
	}

	/**
	 * Publish the event in case an {@link ApplicationEventPublisher} is set.
	 *
	 * @param event can be {@literal null}.
	 */
	protected void publishEvent(RedisKeyExpiredEvent event) {

		if (publisher != null) {
			this.publisher.publishEvent(event);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher)
	 */
	@Override
	public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
		this.publisher = applicationEventPublisher;
	}
}
修改RedisConfig
@Slf4j
@Configuration
public class RedisConfig {

    @Autowired
    private RedisMessageReceiver redisMessageReceiver;

    @Autowired
    private CustomizeMessageListener customizeMessageListener;

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();

        container.setConnectionFactory(connectionFactory);
        
		// 注意以下测试在redis.confi配置文件中设置了: notify-keyspace-events 为 AKE, 
		// 也可以参照KeyspaceEventMessageListener在代码中设置这个配置项
		
        /*
          redis提供的事件通知发布消息示例如下:
             K =>  PUBLISH __keyspace@0__:foo del
             E =>  PUBLISH __keyevent@0__:del foo
          参照上述示例去写这个topic即可
        */

        // 监听key删除事件
        container.addMessageListener(new MessageListener() {

            /*
              执行命令: del order:1234
              输出如下:
                监听key删除事件 - 消息的发布频道: __keyevent@0__:del
                监听key删除事件 - 消息内容: order:1234
                监听key删除事件 - 消息的订阅频道: __keyevent@*__:del
            */

            @Override
            public void onMessage(Message message, byte[] pattern) {
                byte[] bodyBytes = message.getBody();

                byte[] channelBytes = message.getChannel();

                log.info("监听key删除事件 - 消息的发布频道: {}", new String(channelBytes));
                log.info("监听key删除事件 - 消息内容: {}", new String(bodyBytes));
                log.info("监听key删除事件 - 消息的订阅频道: {}", new String(pattern));
            }

        }, new PatternTopic("__keyevent@*__:del"));

        // 监听指定前缀的key
        container.addMessageListener(new MessageListener() {

            @Override
            public void onMessage(Message message, byte[] pattern) {
                byte[] bodyBytes = message.getBody();

                byte[] channelBytes = message.getChannel();

                /*
                  执行命令: set order:1234 a
                  输出如下:
                    监听指定前缀的key - 消息的发布频道: __keyspace@0__:order:1234
                    监听指定前缀的key - 消息内容: set
                    监听指定前缀的key - 消息的订阅频道: __keyspace@0__:order:*
                */

                log.info("监听指定前缀的key - 消息的发布频道: {}", new String(channelBytes));
                log.info("监听指定前缀的key - 消息内容: {}", new String(bodyBytes));
                log.info("监听指定前缀的key - 消息的订阅频道: {}", new String(pattern));
            }

        }, new PatternTopic("__keyspace@0__:order:*"));

        return container;
    }

	/* 借助了
			1. 这个KeyspaceEventMessageListener的bean中的对redis的配置修改
			2. 监听patter的topic
	 */
    @Bean
    public KeyspaceEventMessageListener keyspaceEventMessageListener(RedisMessageListenerContainer container) {
        return new KeyspaceEventMessageListener(container){

            /* __keyevent@* */
            @Override
            protected void doHandleMessage(Message message) {
                log.info("监听所有key命令事件, 消息内容:{}, {}",
                        // set name zzhua; expire name 5;
                        // 消息内容就是key的名称, 比如: name
                        new String(message.getBody()),
                        // 消息所发布的频道, 比如: __keyevent@0__:set, __keyevent@0__:expire等
                        new String(message.getChannel())
                );
            }
        };
    }

    @Bean
    public KeyExpirationEventMessageListener keyExpirationEventMessageListener(RedisMessageListenerContainer container) {
        return new KeyExpirationEventMessageListener(container){

            /* __keyevent@*__:expired */
            @Override
            protected void doHandleMessage(Message message) {
                log.info("监听所有key失效, 消息内容:{}, {}",
                        // 消息内容就是key的名称, 比如: name
                        new String(message.getBody()),
                        // 消息所发布的频道, 比如: __keyevent@0__:expired
                        new String(message.getChannel())
                );
            }
        };
    }

}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/402117.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

运维SRE-16 自动化批量管理-ansible2

7.6ansible-软件包管理模块 yum_repository(管理yum源) yum(yum命令) get_url(wget命令)1&#xff09;yum源配置管理模块 yum源模块 yum_repositoryyum源配置文件内容name[epel]yum源中的名字(中括号里面的名字即可)descriptionnamexxxxxxyum源的注释说明baseurlbaseurlyum源…

一位面试了20+家公司的测试工程师,发现了面试“绝杀四重技”!

年少不懂面试经&#xff0c;读懂已是测试人。 大家好&#xff0c;我是一名历经沧桑&#xff0c;看透互联网行业百态的测试从业者&#xff0c;经过数年的勤学苦练&#xff0c;精钻深研究&#xff0c;终于从初出茅庐的职场新手成长为现在的测试老鸟&#xff0c;早已看透了面试官…

尝试一下最新的联合办公利器ONLYOffice

下载下来一起试试吧 桌面安装版下载地址&#xff1a;https://www.onlyoffice.com/zh/download-desktop.aspx) 官网地址&#xff1a;https://www.onlyoffice.com 普通Office对联合办公的局限性 普通Office软件&#xff08;如Microsoft Office、Google Docs等&#xff09;在面对…

【html学习笔记】3.表单元素

1.文本框 1.1 语法 <input type "text">表示文本框。且只能写一行 1.2 属性 使用属性size 设置文本框大小 <input type"text" size"10">2. 使用属性value 来设置文本框的默认文字 <input type"text" size"…

【初始RabbitMQ】延迟队列的实现

延迟队列概念 延迟队列中的元素是希望在指定时间到了之后或之前取出和处理消息&#xff0c;并且队列内部是有序的。简单来说&#xff0c;延时队列就是用来存放需要在指定时间被处理的元素的队列 延迟队列使用场景 延迟队列经常使用的场景有以下几点&#xff1a; 订单在十分…

js设计模式:依赖注入模式

作用: 在对象外部完成两个对象的注入绑定等操作 这样可以将代码解耦,方便维护和扩展 vue中使用use注册其他插件就是在外部创建依赖关系的 示例: class App{constructor(appName,appFun){this.appName appNamethis.appFun appFun}}class Phone{constructor(app) {this.nam…

开放Gemma而非“开源”,谷歌为何转变大模型竞争策略?

开放Gemma而非“开源”&#xff0c;谷歌为何转变大模型竞争策略 开放而非开源&#xff01;&#xff01;一、Gemma开源模型二、Gemma从今天开始在全球范围内提供。以下是关键的详细信息&#xff1a;三、为什么这样做&#xff1f;四、谷歌这一竞争策略如何&#xff1f; 2月21日晚…

饮用水除氟树脂吸附设备

项目名称 某水务集团地下水除氟项目 工艺选择 石英砂过滤器除氟树脂系统 工艺原理 选择性去除氟化物&#xff0c;降低氯离子、硫酸根的干扰 项目背景 为了保障居民饮水安全与健康&#xff0c;对于含氟量高的地下水必须经过除氟处理&#xff0c;使其符合国家规定的饮用…

【力扣hot100】刷题笔记Day10

前言 一鼓作气把链表给刷完&#xff01;&#xff01;中等题困难题冲冲冲啊啊啊&#xff01; 25. K 个一组翻转链表 - 力扣&#xff08;LeetCode&#xff09; 模拟 class Solution:def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:# 翻转…

having子句

目录 having子句 having和where的区别 Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209645 现在要求查询出每个职位的名称&#xff0c;职位的平均工资&#xff0c;但是要求显示平均工资高于 200 的职位 按照职位先进行分组&#xff0c;同…

四问带你搞懂 I3C

大家都知道 I2C &#xff0c;它的全称是 Inter Integrated Circuit &#xff0c;那 I3C 又是什么&#xff1f; I3C 是 MIPI &#xff08;Mobile Industry Processor Interface&#xff09;移动产业处理器接口联盟推出的&#xff0c;全称是 Improved Inter Integrated Circuit &…

玩转网络抓包利器:Wireshark常用协议分析讲解

Wireshark是一个开源的网络协议分析工具&#xff0c;它能够捕获和分析网络数据包&#xff0c;并以用户友好的方式呈现这些数据包的内容。Wireshark 被广泛应用于网络故障排查、安全审计、教育及软件开发等领域。关于该工具的安装请参考之前的文章&#xff1a;地址 &#xff0c;…

【动态规划专栏】专题四:子数组问题--------最大子数组和环形子数组的最大和

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…

openEuler2203 LTS安装VMware WorkStation Pro 17并远程桌面连接Linux服务器

openEuler 2203 LTS默认只有命令行&#xff0c;没有GUI图形界面&#xff0c;在其中安装VMware WorkStation需要有图形界面的支持。这里以安装深度的DDE桌面环境&#xff0c;最后通过VNC远程桌面连接Linux服务器操作VMware WorkStation。 以下操作请保持网络能正常连接 1、安装…

【网站项目】679学生学籍管理系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

gitlab的使用

前一篇文章我们已经知道Git人人都是中心&#xff0c;那他们怎么交互数据呢&#xff1f; • 使用GitHub或者码云等公共代码仓库 • 使用GitLab私有仓库 目录 一、安装配置gitlab 安装 初始化 这里初始化完成以后需要记住一个初始密码 查看状态 二、使用浏览器访问&#xf…

瑞_VMware虚拟机安装Linux纯净版(含卸载,图文超详细)

文章目录 1 资源准备1.1 官方资源1.2 帮助资源 2 安装 VMware3 安装 CentOS 73.1 镜像 附&#xff1a;VMware删除已安装的操作系统 &#x1f64a; 前言&#xff1a;VMware虚拟机安装Linux纯净版 VMware版本&#xff1a;VMware Workstation 16.2.4Linux版本&#xff1a;CentOS 7…

Stable Diffusion 模型分享:A-Zovya RPG Artist Tools(RPG 大师工具箱)

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八 下载地址 模型介绍 A-Zovya RPG Artist Tools 模型是一个针对 RPG 训练的一个模型&#xff0c;可以生成一些 R…

如何使用eXtplorer部署个人云存储空间并实现公网访问内网数据

文章目录 1. 前言2. eXtplorer网站搭建2.1 eXtplorer下载和安装2.2 eXtplorer网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1. 前言 通过互联网传输文件&#xff0c;是互联网最重要的应用之一&#xff0c;无论是…

内衣洗衣机哪个好用?顶流爆款内衣洗衣机推荐

大家都知道&#xff0c;内衣裤一天不洗&#xff0c;就会滋生很多细菌&#xff0c;很多女生既要忙工作又要忙家务&#xff0c;衣服总会积攒到一堆再去清洗&#xff0c;在潮湿的天气&#xff0c;这样甚至会有发霉的情况出现&#xff0c;而传统的用手洗贴身衣物&#xff0c;看起来…