Redis--延时队列

        我们在日常的java开发里面可能习惯使用RabbitMQ、RocketMQ或Kafka作为消息队列中间件,来给我们的系统增加异步消息传递功能。但是这几个中间件都是专业的消息队列中间件,特性非常多,往往需要花费比较高的时间成本学习。

        在Redis中,当我们需要使用到消息队列的功能,但是又比较单一并且对可靠性要求不高的情况下可以考虑使用Redis实现消息队列功能。

异步消息队列


        我们都知道,队列的特点就是FIFO(先进先出),Redis的list(列表)数据结构常用来作为异步消息队列,使用 lpush 或 rpush 进行入队操作,使用  lpop 或 rpop来出队列。

Java代码实践


我们需要引入spring boot 的redis-starter:



    org.springframework.boot
    spring-boot-starter-data-redis
    2.1.4.RELEASE

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 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 nodes = new HashSet();
		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 genericObjectPoolConfig() {
		GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
		return genericObjectPoolConfig;
	}

	/**
	 * 设置 redisTemplate 序列化方式
	 * 
	 * @param lettuceConnectionFactory
	 * @return
	 */
	@Bean
	public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
		RedisTemplate 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 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 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 obj = redisTemplate.executePipelined(new RedisCallback() {
            @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);
        }
    } 
  

但是我们发现这样的处理还是不够完美,比如,空连接了怎么办,线程一直阻塞在那里怎么办...

所以我们在编写客户端消费者的时候一定要小心,对异常进行捕捉处理...


 

你可能感兴趣的:(Redis学习笔记,redis,java)