概要
通过redis zset实现延时队列
实现
package com.funstar.redisdemo.delay.queue;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.concurrent.*;
/**
* @author funstar
* @date 2019/11/24
*/
@Service
public class DelayQueue implements InitializingBean {
//延时队列在redis中的key
private static final String QUEUE_KEY = "delayQueue";
private static final int poolSize = 3;
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final ThreadFactory consumerThreadFactory = new ThreadFactoryBuilder().setNameFormat("consumer-pool-%d").build();
private static final ExecutorService consumerExecutor = new ThreadPoolExecutor(poolSize, poolSize, 0L,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), consumerThreadFactory);
//消费者开关
private boolean consumerSwitch = false;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void setConsumerSwitch(boolean consumerSwitch) {
this.consumerSwitch = consumerSwitch;
}
@Override
public void afterPropertiesSet() throws Exception {
//poolSize个消费者线程
for (int i = 0; i < poolSize; i++) {
consumerExecutor.execute(new ConsumerJob());
}
}
/**
* 将msg加入到延时队列
*
* @param msg
* @return
*/
public boolean add(String msg) {
LocalDateTime now = LocalDateTime.now();
System.out.println("producer: " + dtf.format(now) + " " + msg);
long score = now.toEpochSecond(ZoneOffset.of("+8")) * 1000 + 5000;//延迟5s
return redisTemplate.opsForZSet().add(QUEUE_KEY, msg, score);
}
class ConsumerJob implements Runnable {
@Override
public void run() {
while (true && !Thread.interrupted()) {
try {
if (!consumerSwitch) {
Thread.sleep(2000);
} else {
Set<Object> values = redisTemplate.opsForZSet().rangeByScore(QUEUE_KEY, 0, System.currentTimeMillis(), 0, 1);
if (CollectionUtils.isEmpty(values)) {
Thread.sleep(1000);
continue;
}
String msg = (String)values.iterator().next();
//如果remove失败,说明其他线程已经删除了这个key,进行下一次查询,保证了线程安全
if (redisTemplate.opsForZSet().remove(QUEUE_KEY, msg) > 0) {
this.handle(msg);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void handle(String msg) {
LocalDateTime now = LocalDateTime.now();
System.out.println("consumer: " + dtf.format(now) + " " + msg + " " + Thread.currentThread().getName());
}
}
}
说明: 生产者:DelayQueue中add操作将消息加入到延时队列,队列数据结构为Redis zset,score为当前时间+5s,即延迟5s 消费者ConsumerJob:consumerSwitch=true时才进行消费,每隔1s查询zset,查询范围是(0,System.currentTimeMillis()),以此来保证不会提前消费,每次获取一个值并从zset中删除,完成一次消费。
package com.funstar.redisdemo.rest;
import com.funstar.redisdemo.delay.queue.DelayQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author funstar
* @date 2019/11/24
*/
@RestController
@RequestMapping("/delayQueue")
public class DelayQueueController {
@Autowired
private DelayQueue delayQueue;
@RequestMapping("/add")
public boolean add(String msg) {
return delayQueue.add(msg);
}
@RequestMapping("/consumerSwitch")
public boolean consumerSwitch(boolean start) {
delayQueue.setConsumerSwitch(start);
return true;
}
}
/add:生产元素
/consumerSwitch:控制消费者开关
结果
producer: 2019-11-25 21:10:16 1
producer: 2019-11-25 21:10:18 2
consumer: 2019-11-25 21:10:21 1 consumer-pool-0
producer: 2019-11-25 21:10:21 3
consumer: 2019-11-25 21:10:23 2 consumer-pool-1
producer: 2019-11-25 21:10:23 4
producer: 2019-11-25 21:10:26 5
consumer: 2019-11-25 21:10:26 3 consumer-pool-2
consumer: 2019-11-25 21:10:28 4 consumer-pool-0
producer: 2019-11-25 21:10:29 6
consumer: 2019-11-25 21:10:31 5 consumer-pool-0
consumer: 2019-11-25 21:10:34 6 consumer-pool-1