https://www.zhihu.com/question/20795043
1.环境
<!-- RedisTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: 192.168.8.128
port: 6379
password: 1234
database: 0
timeout: 4000
jedis:
pool:
max-wait: -1
max-active: -1
max-idle: 20
min-idle: 10
2.配置
package com.yzm.redis08.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ObjectMapperConfig {
public static final ObjectMapper objectMapper;
private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";
static {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
objectMapper = new ObjectMapper()
// 转换为格式化的json(控制台打印时,自动格式化规范)
//.enable(SerializationFeature.INDENT_OUTPUT)
// Include.ALWAYS 是序列化对像所有属性(默认)
// Include.NON_NULL 只有不为null的字段才被序列化,属性为NULL 不序列化
// Include.NON_EMPTY 如果为null或者 空字符串和空集合都不会被序列化
// Include.NON_DEFAULT 属性为默认值不序列化
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
// 如果是空对象的时候,不抛异常
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
// 反序列化的时候如果多了其他属性,不抛出异常
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
// 取消时间的转化格式,默认是时间戳,可以取消,同时需要设置要表现的时间格式
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.setDateFormat(new SimpleDateFormat(PATTERN))
// 对LocalDateTime序列化跟反序列化
.registerModule(javaTimeModule)
.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
// 此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)
;
}
static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.format(DateTimeFormatter.ofPattern(PATTERN)));
}
}
static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext deserializationContext) throws IOException {
return LocalDateTime.parse(p.getValueAsString(), DateTimeFormatter.ofPattern(PATTERN));
}
}
}
package com.yzm.redis08.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import javax.annotation.Resource;
@Configuration
public class RedisConfig {
/**
* redisTemplate配置
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jacksonSerializer.setObjectMapper(ObjectMapperConfig.objectMapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 使用StringRedisSerializer来序列化和反序列化redis的key,value采用json序列化
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(jacksonSerializer);
// 设置hash key 和value序列化模式
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(jacksonSerializer);
template.afterPropertiesSet();
return template;
}
}
3.List队列
在Redis中,List类型是按照插入顺序排序的字符串链表。
和数据结构中的普通链表一样,我们可以在其头部和尾部添加新的元素。
List队列优势:
1.顺序排序,保证先进先出。
2.队列为空时,自动从Redis数据库删除。
3.在队列的两头插入或删除元素,效率极高,即使队列中元素达到百万级。
4.List中可以包含的最大元素数量是4294967295
生产消息
package com.yzm.redis08.message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MessageProducer {
public static final String MESSAGE_KEY = "message:queue";
private final StringRedisTemplate redisTemplate;
public MessageProducer(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void lPush() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Long size = redisTemplate.opsForList().leftPush(MESSAGE_KEY, Thread.currentThread().getName() + ":hello world");
log.info(Thread.currentThread().getName() + ":put message size = " + size);
}).start();
}
}
}
消费消息,定时器以达到监听队列功能
package com.yzm.redis08.message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@EnableScheduling
public class MessageConsumer {
public static final String MESSAGE_KEY = "message:queue";
private final StringRedisTemplate redisTemplate;
public MessageConsumer(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Scheduled(initialDelay = 5 * 1000, fixedRate = 2 * 1000)
public void rPop() {
String message = redisTemplate.opsForList().rightPop(MESSAGE_KEY);
log.info(message);
}
}
package com.yzm.redis08.controller;
import com.yzm.redis08.message.MessageProducer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RedisController {
public final MessageProducer messageProducer;
public RedisController(MessageProducer messageProducer) {
this.messageProducer = messageProducer;
}
@GetMapping("/lPush")
public void lPush() {
messageProducer.lPush();
}
}
http://localhost:8080/lPush
存在的问题:
1.通过定时器监听List中是否有待处理消息,每执行一次都会发起一次连接,这会造成不必要的浪费。
2.生产速度大于消费速度,队列堆积,消息时效性差,占用内存。
阻塞消费
package com.yzm.redis08.message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@EnableScheduling
public class MessageConsumer {
public static final String MESSAGE_KEY = "message:queue";
private final StringRedisTemplate redisTemplate;
public MessageConsumer(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
// @Scheduled(initialDelay = 5 * 1000, fixedRate = 2 * 1000)
public void rPop() {
String message = redisTemplate.opsForList().rightPop(MESSAGE_KEY);
log.info(message);
}
@PostConstruct
public void brPop() {
new Thread(() -> {
while (true) {
String message = redisTemplate.opsForList().rightPop(MESSAGE_KEY, 10, TimeUnit.SECONDS);
log.info(message);
}
}).start();
}
}
当队列没有元素时,会阻塞10秒,然后再次监听队列,
需要注意的是,阻塞时间必须小于连接超时时间
阻塞时间不能为负,直接报错超时为负
阻塞时间为零,此时阻塞时间等于超时时间,最后报错连接超时
阻塞时间大于超时时间,报错连接超时
http://localhost:8080/lPush
死循环监听队列,当队列没有元素时,阻塞队列。
缺点:
1.消息不可重复消费,因为消息从队列POP之后就被移除了,即不支持多个消费者消费同一批数据
2.消息丢失,消费期间发生异常,消息未能正常消费
4.发布/订阅模式
类似于MQ的主题模式。
消息可以重复消费,多个消费者订阅同一频道即可。
一个消费者根据匹配规则订阅多个频道。
消费者只能消费订阅之后发布的消息,这意味着,消费者下线再上线这期间发布的消息将会丢失。
数据不具有持久化。同样Redis宕机也会数据丢失。
消息发布后,是推送到一个缓冲区(内存),消费者从缓冲区拉取消息,当消息堆积,缓冲区溢出,消费者就会被迫下线,同时释放对应的缓冲区。
RedisConfig中添加监听器
/**
* redis消息监听器容器
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//订阅频道,通配符*表示任意多个占位符
container.addMessageListener(new MySubscribe(), new PatternTopic("channel*"));
return container;
}
订阅者
package com.yzm.redis08.message;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
public class MySubscribe implements MessageListener {
@Override
public void onMessage(Message message, byte[] bytes) {
System.out.println("订阅频道:" + new String(message.getChannel()));
System.out.println("接收数据:" + new String(message.getBody()));
}
}
消息发布
@RestController
public class RedisController {
public final MessageProducer messageProducer;
public final RedisTemplate<String, Object> redisTemplate;
public RedisController(MessageProducer messageProducer, RedisTemplate<String, Object> redisTemplate2) {
this.messageProducer = messageProducer;
this.redisTemplate = redisTemplate2;
}
@GetMapping("/lPush")
public void lPush() {
messageProducer.lPush();
}
@GetMapping("/publish")
public void publish() {
redisTemplate.convertAndSend("channel_first", "hello world");
}
}
http://localhost:8080/publish
另一种订阅方式
package com.yzm.redis08.message;
public class MySubscribe2 {
public void getMessage(Object message, String channel) {
System.out.println("订阅频道2:" + channel);
System.out.println("接收数据2:" + message);
}
}
/**
* redis消息监听器容器
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//订阅频道,通配符*:表示任意多个占位符
container.addMessageListener(new MySubscribe(), new PatternTopic("channel*"));
// 通配符?:表示一个占位符
MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(new MySubscribe2(), "getMessage");
listenerAdapter.afterPropertiesSet();
container.addMessageListener(listenerAdapter, new PatternTopic("channel?"));
return container;
}
@GetMapping("/publish2")
public void publish2() {
redisTemplate.convertAndSend("channel2", "hello world");
}
http://localhost:8080/publish2
消息是实体对象,进行转换
package com.yzm.redis08.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 5250232737975907491L;
private Integer id;
private String username;
}
package com.yzm.redis08.message;
import com.yzm.redis08.config.ObjectMapperConfig;
import com.yzm.redis08.entity.User;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
public class MySubscribe3 implements MessageListener {
@Override
public void onMessage(Message message, byte[] bytes) {
Jackson2JsonRedisSerializer<User> jacksonSerializer = new Jackson2JsonRedisSerializer<>(User.class);
jacksonSerializer.setObjectMapper(ObjectMapperConfig.objectMapper);
User user = jacksonSerializer.deserialize(message.getBody());
System.out.println("订阅频道3:" + new String(message.getChannel()));
System.out.println("接收数据3:" + user);
}
}
/**
* redis消息监听器容器
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//订阅频道,通配符*:表示任意多个占位符
container.addMessageListener(new MySubscribe(), new PatternTopic("channel*"));
// 通配符?:表示一个占位符
MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(new MySubscribe2(), "getMessage");
listenerAdapter.afterPropertiesSet();
container.addMessageListener(listenerAdapter, new PatternTopic("channel?"));
container.addMessageListener(new MySubscribe3(), new PatternTopic("user"));
return container;
}
@GetMapping("/publish3")
public void publish3() {
User user = User.builder().id(1).username("yzm").build();
redisTemplate.convertAndSend("user", user);
}
http://localhost:8080/publish3
5 ZSet 实现延时队列
生产消息,score=时间戳+60s随机数
public void zadd() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
int increment = ati.getAndIncrement();
log.info(Thread.currentThread().getName() + ":put message to zset = " + increment);
double score = System.currentTimeMillis() + new Random().nextInt(60 * 1000);
redisTemplate.opsForZSet().add(MESSAGE_ZKEY, Thread.currentThread().getName() + " hello zset:" + increment, score);
}).start();
}
}
定时任务,每秒执行一次
@Scheduled(initialDelay = 5 * 1000, fixedRate = 1000)
public void zrangebysocre() {
log.info("延时队列消费。。。");
// 拉取score小于当前时间戳的消息
Set<String> messages = redisTemplate.opsForZSet().rangeByScore(MESSAGE_ZKEY, 0, System.currentTimeMillis());
if (messages != null) {
for (String message : messages) {
Double score = redisTemplate.opsForZSet().score(MESSAGE_ZKEY, message);
log.info("消费了:" + message + "消费时间为:" + simpleDateFormat.format(score));
redisTemplate.opsForZSet().remove(MESSAGE_ZKEY, message);
}
}
}
@GetMapping("/zadd")
public void zadd() {
messageProducer.zadd();
}
http://localhost:8080/zadd