没技术怎么做网站,WordPress二级目录404,搭建wordpress环境搭建,网站制作 php目录 一、简介1、介绍2、对比 二、整合spring的data-redis实现1、使用依赖2、配置类2.1、配置RedisTemplate bean2.2、异常类 3、实体类3.1、User3.2、Book 4、发送消息4.1、RedisStreamUtil工具类4.2、通过延时队列线程池模拟发送消息4.3、通过http主动发送消息 5、#x1f3… 目录 一、简介1、介绍2、对比 二、整合spring的data-redis实现1、使用依赖2、配置类2.1、配置RedisTemplate bean2.2、异常类 3、实体类3.1、User3.2、Book 4、发送消息4.1、RedisStreamUtil工具类4.2、通过延时队列线程池模拟发送消息4.3、通过http主动发送消息 5、消息接收5.1、不绑定消费组---可以实现广播效果方式1主动读取测试日志 方式2通过监听器监听是否有新消息 5.2、指定消费组----实现一个组内只有一个成员可以接收到5.2.1、配置类5.2.2、监听器通过延时队列发送到消息---测试结果通过http发送User到子类Book对象数据测试结果 三、完整代码四、引用 背景 使用该方式实现主要是因为项目中有个地方刚好需要异步来实现而项目又没有配置专业的消息中间件并且使用的也不是太频繁就觉得没必要专门安装一个MQ服务了直接通过现有的redis的stream来实现异步消息接收直接具体的业务逻辑。 一、简介
1、介绍
Redis StreamRedis Streams是Redis 5.0版本引入的一种数据结构用于处理时间序列数据、消息队列和日志流。它提供了高吞吐量、持久性、有序、可扩展的消息传递解决方案。Redis Stream 结构是对传统发布/订阅模式的增强使你能够更灵活地处理数据流并提供了以下主要特性 多生产者和多消费者多个生产者可以同时向 Stream 中写入消息而多个消费者可以独立订阅并消费消息。每个消费者可以有不同的消费速率。 消费组Redis Stream引入了消费者组的概念多个消费者可以加入同一个消费者组并共同消费消息这确保了消息在消费时不会被多次处理。 消费者阻塞消费者可以使用 XREADGROUP 命令以阻塞方式获取消息只有当有新消息到达时才会被推送给消费者。 消费者自动确认Redis Stream 支持自动确认消息消费者可以告诉 Redis 何时确认已经成功处理了一条消息。 多 Stream 支持你可以创建多个 Stream 来存储不同种类的数据并分别处理它们。 有序性消息在 Stream 中按照消息的时间戳有序存储因此你可以按照消息的顺序读取数据。 持久性存储Redis Stream 使用内存数据结构但也支持将数据异步保存到磁盘以确保数据不会丢失。
2、对比
对比redis的其他几种实现方式来说功能更加全面支持可持久化和通过ack确认的方式基本实现了消息丢失的问题当然对比专业的消息队列中间件来说还是有些不足的。 需要看详细对比可以看 redis队列对比 这篇文章
二、整合spring的data-redis实现
1、使用依赖
dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactIdversion2.11.1/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependency/dependencies2、配置类
2.1、配置RedisTemplate bean
重点是下面这一句不能用json的序列化类否则会序列化失败 redisTemplate.setHashValueSerializer(RedisSerializer.string()); Beanpublic RedisTemplateString, Object redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplateString, Object redisTemplate new RedisTemplate();redisTemplate.setConnectionFactory(connectionFactory);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new StringRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());// 这个地方不可使用 json 序列化如果使用的是ObjectRecord传输对象时可能会有问题会出现一个 java.lang.IllegalArgumentException: Value must not be null! 错误redisTemplate.setHashValueSerializer(RedisSerializer.string());return redisTemplate;}2.2、异常类
Slf4j
public class CustomErrorHandler implements ErrorHandler {Overridepublic void handleError(Throwable throwable) {log.error(发生了异常,throwable);}
}3、实体类
该地方使用了两个实体类主要是用于测试如果不是指定的同一个类型时指定的是父类的类型是否可以正常反序列化接收消息
3.1、User
Data
NoArgsConstructor
AllArgsConstructor
ToString
public class Book extends User{private String title;private String author;
}
3.2、Book
Data
NoArgsConstructor
AllArgsConstructor
ToString
public class User implements Serializable {private String name;private Integer age;
}4、发送消息
发送消息主要是通过redisTemplate.opsForStream().add(record);进行发送到redis中接收时会分两种方式接收看后续
4.1、RedisStreamUtil工具类
用于实现消息发送、初始化组、key绑定组、清除消费了的消息等方法 在第一次发送消息时需要先绑定接收的组和可key否则在接收时会报不存在该组的异常发送消息后需要把该条消费了的消息清除掉否则会一直保持在stream中 Component
Slf4j
public class RedisStreamUtil {public static final String STREAM_KEY_001 stream-001;Resourceprivate RedisTemplateString,Object redisTemplate;/*** 添加记录到流中* param streamKey* param t* param T*/public T RecordId add(String streamKey,T t){ObjectRecordString, T record StreamRecords.newRecord().in(streamKey) //key.ofObject(t) //消息数据.withId(RecordId.autoGenerate());
// 发送消息RecordId recordId redisTemplate.opsForStream().add(record);log.info(添加成功返回的record-id[{}],recordId);return recordId;}/*** 用来创建绑定流和组*/public void addGroup(String key, String groupName){redisTemplate.opsForStream().createGroup(key,groupName);}/*** 用来判断key是否存在*/public boolean hasKey(String key){if(keynull){return false;}else{return redisTemplate.hasKey(key);}}/*** 用来删除掉消费了的消息*/public void delField(String key,RecordId recordIds){redisTemplate.opsForStream().delete(key,recordIds);}/*** 用来初始化 实现绑定key和消费组*/public void initStream(String key, String group){//判断key是否存在如果不存在则创建boolean hasKey hasKey(key);if(!hasKey){MapString,Object map new HashMap();map.put(key,value);RecordId recordId add(key, map);addGroup(key,group); //第一次初始化时需要把Stream和group绑定delField(key,recordId); //清除掉该条无用数据log.info(stream:{}-group:{} initialize success,key,group);}}public String getStreamKey001(){return STREAM_KEY_001;}
}
4.2、通过延时队列线程池模拟发送消息 该方法里通过模拟延时5秒后每隔3秒发送一条数据发送10条后关闭线程池 /*** 在spring初始化时执行定时发送消息到stream中用于模拟发送消息*/
//Component
public class StreamMessageRunner implements ApplicationRunner {Resourceprivate RedisStreamUtil redisStreamUtil;Overridepublic void run(ApplicationArguments args) throws Exception {ScheduledExecutorService pool Executors.newScheduledThreadPool(2);AtomicInteger integer new AtomicInteger(0);//使用延时队列线程池模拟发送数据消息pool.scheduleAtFixedRate(()-{User zhangsan new User(zhangsaninteger.get(), 1 integer.get());RecordId recordId redisStreamUtil.add(redisStreamUtil.getStreamKey001(), zhangsan);integer.getAndIncrement();
//需要把消费了的消息清除掉否则会一直保持在stream中会被重复消费redisStreamUtil.delField(redisStreamUtil.getStreamKey001(),recordId);if (integer.get()10){System.out.println(---------退出发送消息--------);pool.shutdown();}},5,3, TimeUnit.SECONDS);}
}4.3、通过http主动发送消息 通过分别发送父类和子类比对查看不同效果 RestController
RequestMapping(/index)
public class index {Resourceprivate RedisStreamUtil redisStreamUtil;/*** 父类*/GetMapping(/login)public String login(User user){RecordId recordId redisStreamUtil.add(redisStreamUtil.getStreamKey001(), user);redisStreamUtil.delField(redisStreamUtil.getStreamKey001(),recordId);return 成功;}/*** 子类*/GetMapping(/login2)public String login(Book book){RecordId recordId redisStreamUtil.add(redisStreamUtil.getStreamKey001(), book);redisStreamUtil.delField(redisStreamUtil.getStreamKey001(),recordId);return 成功;}
}5、消息接收
5.1、不绑定消费组—可以实现广播效果
节点消费者不绑定消费组直接和stream进行绑定即可实现广播的效果每次有消息发送到该指定节点的stream都可以接收到。 如下图有消息发送到redis Stream 里面绑定的A0到B2全部可以接收到这条消息 方式1主动读取
通过redisTemplate.opsForStream().read()方法主动去stream中读取消息
/*** 独立消费者---可以读取到该key的全部消息*/
Component
Slf4j
public class XreadNonBlockConsumer01 implements InitializingBean, DisposableBean {private ThreadPoolExecutor threadPoolExecutor;Resourceprivate RedisTemplateString,Object redisTemplate;private volatile boolean stop false;/*** 初始化bean时执行以轮询的方式主动去stream的指定key里读取消息* throws Exception*/Overridepublic void afterPropertiesSet() throws Exception {// 初始化线程池threadPoolExecutor new ThreadPoolExecutor(3, 5, 0, TimeUnit.SECONDS,new LinkedBlockingDeque(), r - {Thread thread new Thread(r);thread.setDaemon(true);thread.setName(xread-nonblock-01);return thread;});StreamReadOptions options StreamReadOptions.empty()
// 如果没有数据则阻塞1s阻塞时间需要小于·spring.redis.timeout·.block(Duration.ofMillis(1000))
// 一直阻塞直到获取数据可能会报超时异常
// .block(Duration.ofMillis(0))
// 一次获取10条数据.count(10);StringBuilder readBuilder new StringBuilder(0-0);threadPoolExecutor.execute(()-{while (!stop){//主动到redis的stream中去读取options设置了每读取一次阻塞一秒ListObjectRecordString, User objectRecords redisTemplate.opsForStream().read(User.class, options,StreamOffset.create(RedisStreamUtil.STREAM_KEY_001, ReadOffset.from(readBuilder.toString())));if (CollectionUtils.isEmpty(objectRecords)){log.warn(没有读取到数据);continue;}objectRecords.stream().forEach(objectRecord-{log.info(获取到的数据信息 id:[{}] book:[{}], objectRecord.getId(), objectRecord.getValue());readBuilder.setLength(0);readBuilder.append(objectRecord.getId());});}});}/*** 在销毁bean时把线程池关闭* throws Exception*/Overridepublic void destroy() throws Exception {stop true;threadPoolExecutor.shutdown();threadPoolExecutor.awaitTermination(3,TimeUnit.SECONDS);}}测试日志 方式2通过监听器监听是否有新消息
具体代码和分组的是一样的只不过不指定组而已就合并在下面写了 主要通过StreamMessageListenerContainer这个监听器类实现。
主要通过下面这一句 container.receive(StreamOffset.fromStart(RedisStreamUtil.STREAM_KEY_001), new MonitorStreamListener(独立消费, null, null)); 5.2、指定消费组----实现一个组内只有一个成员可以接收到
进行分组之后一个组内只会有一个成员可以读到消息具体如下图当然在使用时也可以绑定多个组每个组接收不听的消息。下面方式就相当于mq了交换机队列和路由键的关系
5.2.1、配置类
下面代码具体流程时先创建一个线程池然后在配置消息监听容器最后在把用于接收消息的监听器放入到监听容器中去最后把这个侦听容器注入到bean去
Configuration
public class RedisStreamConfiguration {Resourceprivate RedisStreamUtil redisStreamUtil;Resourceprivate RedisConnectionFactory redisConnectionFactory;Bean(initMethod start,destroyMethod stop)public StreamMessageListenerContainerString, ObjectRecordString,User streamMessageListenerContainer(){AtomicInteger index new AtomicInteger(1);
// 获取本机线程数int processors Runtime.getRuntime().availableProcessors();ThreadPoolExecutor executor new ThreadPoolExecutor(processors, processors, 0,TimeUnit.SECONDS,new ArrayBlockingQueue(50), (r) - {Thread thread new Thread(r);thread.setName(async-stream-consumer- index.getAndIncrement());thread.setDaemon(true);return thread;}, new ThreadPoolExecutor.CallerRunsPolicy());// 消息监听容器不能在外部实现。创建后StreamMessageListenerContainer可以订阅Redis流并使用传入的消息StreamMessageListenerContainer.StreamMessageListenerContainerOptionsString,ObjectRecordString,User options StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()// 一次最多获取多少条消息.batchSize(10)// 运行 Stream 的 poll task.executor(executor)// Stream 中没有消息时阻塞多长时间需要比 spring.redis.timeout 的时间小.pollTimeout(Duration.ofSeconds(1))// ObjectRecord 时将 对象的 filed 和 value 转换成一个 Map 比如将Book对象转换成map
// .objectMapper(new ObjectHashMapper())// 获取消息的过程或获取到消息给具体的消息者处理的过程中发生了异常的处理.errorHandler(new CustomErrorHandler())// 将发送到Stream中的Record转换成ObjectRecord转换成具体的类型是这个地方指定的类型.targetType(User.class).build();StreamMessageListenerContainerString, ObjectRecordString, User container StreamMessageListenerContainer.create(redisConnectionFactory, options);// 初始化-绑定key和消费组redisStreamUtil.initStream(RedisStreamUtil.STREAM_KEY_001,group-a);// 不绑定消费组独立消费container.receive(StreamOffset.fromStart(RedisStreamUtil.STREAM_KEY_001), new MonitorStreamListener(独立消费, null, null));// 消费组A,不自动ack// 从消费组中没有分配给消费者的消息开始消费
// container.receive(Consumer.from(group-a,consumer-a),
// StreamOffset.create(RedisStreamUtil.STREAM_KEY_001, ReadOffset.lastConsumed()),new MonitorStreamListener(消费者组A,group-a, consumer-a));// 自动ackcontainer.receiveAutoAck(Consumer.from(group-a,consumer-b),StreamOffset.create(RedisStreamUtil.STREAM_KEY_001, ReadOffset.lastConsumed()),new MonitorStreamListener(消费者组B,group-a, consumer-b));return container;}
}重要代码解析 .targetType(User.class) 在配置监听容器时用于指定类型不指定时默认是string类型如果你传入的不是string机需要指定如果配置的是父类的也可以接收子类的消息进行转换。但如果是配置的Object类型接收时就会为路径不能正常得到传入的对象不知道为什么有研究懂的可以解答一下redisStreamUtil.initStream(RedisStreamUtil.STREAM_KEY_001,group-a)在第一次生成时需要把消费组绑定该stream的key否则会报错具体内部执行逻辑可以看initStream()方法或者自己手动通过命令到redis去绑定xgroup create stream-001 group-a $stream-001(key) group-a(消费组)container.receive(StreamOffset.fromStart(RedisStreamUtil.STREAM_KEY_001), new MonitorStreamListener(独立消费, null, null)) 该句是不绑定消费组也就是广播的方式监听该key中的所有消息和上面的区别是该方式是被动的监听消息container.receiveAutoAck(Consumer.from(group-a,consumer-b) ,StreamOffset.create(RedisStreamUtil.STREAM_KEY_001, ReadOffset.lastConsumed()),new MonitorStreamListener(消费者组B,group-a, consumer-b))就是通过该句代码实现分组监听消息的绑定了消费组和消费者的名字以及监听器类。然后使用的自动ack的方式回复stream确认接收到了消息(或者通过手动ack的方式返回stream接收到了消息否则会重复发送) 5.2.2、监听器
用于接收消息然后实现具体业务代码
Slf4j
public class MonitorStreamListener T implements StreamListenerString, ObjectRecordString,T {/*** 消费者类型独立消费、消费组消费*/private String consumerType;/*** 消费组*/private String group;/*** 消费组中的某个消费者*/private String consumerName;public MonitorStreamListener(String consumerType, String group, String consumerName) {this.consumerType consumerType;this.group group;this.consumerName consumerName;}Overridepublic void onMessage(ObjectRecordString, T message) {log.info(接受到来自redis的消息);String stream message.getStream();RecordId id message.getId();User value (User) message.getValue();value.getName();// 执行具体的接收到消息的业务逻辑if (StringUtils.isEmpty(group)) {log.info([{}]: 接收到一个消息 stream:[{}],id:[{}],value:[{}], consumerType, stream, id, value);} else {log.info([{}] group:[{}] consumerName:[{}] 接收到一个消息 stream:[{}],id:[{}],value:[{}], consumerType,group, consumerName, stream, id, value);}// 当是消费组消费时如果不是自动ack则需要在这个地方手动ack
// redisTemplate.opsForStream()
// .acknowledge(key,group,recordId);}
}通过延时队列发送到消息—测试结果 通过http发送User到子类Book对象数据测试结果 结果也是可以正常接受到的
三、完整代码
完整代码仓库地址
四、引用
https://juejin.cn/post/7029302992364896270#heading-0 https://juejin.cn/post/6844904125822435341searchId202310141054532F9807A1000F6680C0DF#heading-1