我们在日常的java开发里面可能习惯使用RabbitMQ、RocketMQ或Kafka作为消息队列中间件,来给我们的系统增加异步消息传递功能。但是这几个中间件都是专业的消息队列中间件,特性非常多,往往需要花费比较高的时间成本学习。
在Redis中,当我们需要使用到消息队列的功能,但是又比较单一并且对可靠性要求不高的情况下可以考虑使用Redis实现消息队列功能。
异步消息队列
我们都知道,队列的特点就是FIFO(先进先出),Redis的list(列表)数据结构常用来作为异步消息队列,使用 lpush 或 rpush 进行入队操作,使用 lpop 或 rpop来出队列。
Java代码实践
我们需要引入spring boot 的redis-starter:
<!-- 整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
Redis的application.properties配置:(我用的是redis集群3m-3s,单节点配置类似)
#redis cluster config
#RedisCluster集群节点及端口信息
spring.redis.cluster.nodes=192.168.50.29:6380,192.168.50.29:6381,192.168.50.29:6382,192.168.50.29:6383,192.168.50.29:6384,192.168.50.29:6385
#Redis密码
spring.redis.password=
#在群集中执行命令时要遵循的最大重定向数目
spring.redis.cluster.max-redirects=6
#Redis连接池在给定时间可以分配的最大连接数。使用负值无限制
spring.redis.jedis.pool.max-active=1000
#以毫秒为单位的连接超时时间
spring.redis.timeout=2000
#池中“空闲”连接的最大数量。使用负值表示无限数量的空闲连接
spring.redis.jedis.pool.max-idle=8
#目标为保持在池中的最小空闲连接数。这个设置只有在设置max-idle的情况下才有效果
spring.redis.jedis.pool.min-idle=5
#连接分配在池被耗尽时抛出异常之前应该阻塞的最长时间量(以毫秒为单位)。使用负值可以无限期地阻止
spring.redis.jedis.pool.max-wait=1000
#redis cluster只使用db0
spring.redis.index=0
Redis集群配置:
@Configuration
public class RedisConfig {
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private Long timeout;
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Value("${spring.redis.cluster.max-redirects}")
private Integer maxRedirects;
@Bean
public LettuceConnectionFactory lettuceConnectionFactory(GenericObjectPoolConfig<Object> genericObjectPoolConfig) {
// 单机版配置
// RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
// redisStandaloneConfiguration.setDatabase(database);
// redisStandaloneConfiguration.setHostName(host);
// redisStandaloneConfiguration.setPort(port);
// redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
// 集群版配置
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
String[] serverArray = clusterNodes.split(",");
Set<RedisNode> nodes = new HashSet<RedisNode>();
for (String ipPort : serverArray) {
String[] ipAndPort = ipPort.split(":");
nodes.add(new RedisNode(ipAndPort[0].trim(), Integer.valueOf(ipAndPort[1])));
}
redisClusterConfiguration.setPassword(RedisPassword.of(password));
redisClusterConfiguration.setClusterNodes(nodes);
redisClusterConfiguration.setMaxRedirects(maxRedirects);
LettuceClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
.commandTimeout(Duration.ofMillis(timeout)).poolConfig(genericObjectPoolConfig).build();
//单机
// LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration,
// lettuceClientConfiguration);
//集群
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisClusterConfiguration,
lettuceClientConfiguration);
return lettuceConnectionFactory;
}
/**
* GenericObjectPoolConfig 连接池配置
*
* @return
*/
@Bean
public GenericObjectPoolConfig<Object> genericObjectPoolConfig() {
GenericObjectPoolConfig<Object> genericObjectPoolConfig = new GenericObjectPoolConfig<Object>();
return genericObjectPoolConfig;
}
/**
* 设置 redisTemplate 序列化方式
*
* @param lettuceConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
通过接入spring boot 的web模块,使用接口和定时器的模拟生产者和消费者;
入队:
@GetMapping("/notifyQueue")
@ApiOperation("延时队列")
public JSONResult notifyQueue(){
List<Object> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("pear");
//入队
redisUtil.lSet("notity-queue", list);
//读取队列的大小
long size = redisUtil.lGetListSize("notity-queue");
log.info("列表长度:"+size);
return JSONResult.ok();
}
Redis入队的方法:
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().leftPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
定时器模拟出队(消费者):
@Component
@Configurable
@EnableScheduling
@Slf4j
public class SchduleExector {
@Autowired
private RedisUtil redisUtil;
@Scheduled(cron = " 0/1 * * * * ? ")
public void consumerList(){
String value = redisUtil.rgetList("notity-queue");
if(StringUtils.isNotEmpty(value)){
log.info("模拟处理业务");
}
}
}
Redis出队方法:
public String rgetList(String key) {
return (String) redisTemplate.opsForList().rightPop(key);
}
把服务运行起来,请求接口后,我们可以看到这样的结果:
这样我们就简单的模拟了Redis的队列了,但是这个时候有人可能注意到了,当我pop的时候每次都是空的怎么办?那样不会影响到Redis的性能吗?
队列空了怎么办?
如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据。这就是浪费生命的空轮询。空轮询不但拉高了客户端的 CPU, redis 的 QPS 也会被拉高,如果这样空轮询的客户端有几十来个, Redis 的慢查询可能会显著增多。
我们可以每次取前先睡一会,这样来缓解Redis的压力。
队列延迟
有没有什么办法能显著降低延迟呢?你当然可以很快想到:那就把睡觉的时间缩短点。这种方式当然可以,不过有没有更好的解决方案呢?当然也有,那就是 blpop/brpop。
阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。用 blpop/brpop 替代前面的 lpop/rpop,就完美解决了上面的问题。
public void brpopList(String key) {
System.out.println("进入阻塞方法");
List<Object> obj = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Nullable
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
return connection.bLPop(10, key.getBytes());
}
},new StringRedisSerializer());
for (Object str: obj) {
System.out.println("blockingConsume : "+str);
}
}
但是我们发现这样的处理还是不够完美,比如,空连接了怎么办,线程一直阻塞在那里怎么办...
所以我们在编写客户端消费者的时候一定要小心,对异常进行捕捉处理...