|
@@ -1,361 +0,0 @@
|
|
|
-package com.chelvc.framework.redis.queue;
|
|
|
-
|
|
|
-import java.time.Duration;
|
|
|
-import java.util.AbstractQueue;
|
|
|
-import java.util.Collection;
|
|
|
-import java.util.Collections;
|
|
|
-import java.util.Iterator;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Map;
|
|
|
-import java.util.Objects;
|
|
|
-import java.util.Set;
|
|
|
-import java.util.function.Function;
|
|
|
-
|
|
|
-import com.chelvc.framework.base.context.ApplicationContextHolder;
|
|
|
-import com.chelvc.framework.common.util.AssertUtils;
|
|
|
-import com.chelvc.framework.common.util.ObjectUtils;
|
|
|
-import com.chelvc.framework.common.util.StringUtils;
|
|
|
-import com.chelvc.framework.redis.context.RedisContextHolder;
|
|
|
-import com.google.common.collect.ImmutableMap;
|
|
|
-import com.google.common.collect.Maps;
|
|
|
-import lombok.NonNull;
|
|
|
-import org.springframework.dao.DataAccessException;
|
|
|
-import org.springframework.data.redis.core.RedisOperations;
|
|
|
-import org.springframework.data.redis.core.RedisTemplate;
|
|
|
-import org.springframework.data.redis.core.SessionCallback;
|
|
|
-import org.springframework.data.redis.core.script.DefaultRedisScript;
|
|
|
-import org.springframework.data.redis.core.script.RedisScript;
|
|
|
-
|
|
|
-/**
|
|
|
- * 带临时性的Redis队列
|
|
|
- *
|
|
|
- * @param <E> 元素类型
|
|
|
- * @author Woody
|
|
|
- * @date 2024/8/29
|
|
|
- */
|
|
|
-public class TemporalRedisQueue<E> extends AbstractQueue<E> {
|
|
|
- /**
|
|
|
- * 队列名称/实例映射表
|
|
|
- */
|
|
|
- @SuppressWarnings("rawtypes")
|
|
|
- private static final Map<String, TemporalRedisQueue> INSTANCES = Maps.newConcurrentMap();
|
|
|
-
|
|
|
- /**
|
|
|
- * Redis队列添加元素脚本(如果存在则忽略)
|
|
|
- */
|
|
|
- private static final RedisScript<Long> ADD_SCRIPT = new DefaultRedisScript<>(
|
|
|
- "return redis.call('ZADD', KEYS[1], 'NX', 'CH', unpack(ARGV))", Long.class
|
|
|
- );
|
|
|
-
|
|
|
- /**
|
|
|
- * Redis队列弹出元素脚本
|
|
|
- */
|
|
|
- @SuppressWarnings("rawtypes")
|
|
|
- private static final RedisScript<List> POLL_SCRIPT = new DefaultRedisScript<>(
|
|
|
- "local value = redis.call('ZRANGEBYSCORE', KEYS[1], ARGV[1], ARGV[2], 'LIMIT', 0, 1) " +
|
|
|
- "if value[1] then redis.call('ZADD', KEYS[1], 'XX', ARGV[3], value[1]) end return value", List.class
|
|
|
- );
|
|
|
-
|
|
|
- private final long idle;
|
|
|
- private final long timeout;
|
|
|
- private final String name;
|
|
|
- private final List<String> keys;
|
|
|
-
|
|
|
- public TemporalRedisQueue(@NonNull String name) {
|
|
|
- this(name, Duration.ofMinutes(1), Duration.ZERO);
|
|
|
- }
|
|
|
-
|
|
|
- public TemporalRedisQueue(@NonNull String name, @NonNull Duration idle) {
|
|
|
- this(name, idle, Duration.ZERO);
|
|
|
- }
|
|
|
-
|
|
|
- public TemporalRedisQueue(@NonNull String name, @NonNull Duration idle, @NonNull Duration timeout) {
|
|
|
- String profile = ApplicationContextHolder.getProfile();
|
|
|
- this.name = StringUtils.isEmpty(profile) ? name : (profile + "-" + name);
|
|
|
- this.idle = idle.toMillis();
|
|
|
- AssertUtils.check(this.idle > 0, () -> "idle must be greater than 0");
|
|
|
- this.timeout = timeout.toMillis();
|
|
|
- this.keys = Collections.singletonList(this.name);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 集合迭代器实现
|
|
|
- */
|
|
|
- private class Iter implements Iterator<E> {
|
|
|
- private E element;
|
|
|
- private int index = 0;
|
|
|
- private final int size;
|
|
|
-
|
|
|
- public Iter(int size) {
|
|
|
- AssertUtils.check(size > -1, () -> "size must be greater than -1");
|
|
|
- this.size = size;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean hasNext() {
|
|
|
- return this.index < this.size;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public E next() {
|
|
|
- Set<E> values = template().opsForZSet().range(name, this.index, this.index++);
|
|
|
- return ObjectUtils.isEmpty(values) ? null : (this.element = values.iterator().next());
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void remove() {
|
|
|
- if (this.element != null) {
|
|
|
- template().opsForZSet().remove(name, this.element);
|
|
|
- this.element = null;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取消息队列
|
|
|
- *
|
|
|
- * @param name 队列名称
|
|
|
- * @param <E> 消息类型
|
|
|
- * @return 队列实例
|
|
|
- */
|
|
|
- public static <E> TemporalRedisQueue<E> get(@NonNull String name) {
|
|
|
- return get(name, TemporalRedisQueue::new);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取消息队列
|
|
|
- *
|
|
|
- * @param name 队列名称
|
|
|
- * @param builder 队列构建函数
|
|
|
- * @param <E> 消息类型
|
|
|
- * @return 队列实例
|
|
|
- */
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- public static <E> TemporalRedisQueue<E> get(@NonNull String name,
|
|
|
- @NonNull Function<String, TemporalRedisQueue<E>> builder) {
|
|
|
- TemporalRedisQueue<E> queue = INSTANCES.get(name);
|
|
|
- return queue == null ? INSTANCES.computeIfAbsent(name, builder) : queue;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 添加元素
|
|
|
- *
|
|
|
- * @param args 添加参数
|
|
|
- * @return true/false
|
|
|
- */
|
|
|
- private boolean add(Object[] args) {
|
|
|
- Long count;
|
|
|
- if (this.timeout > 0) {
|
|
|
- long max = System.currentTimeMillis() - this.timeout - 1000;
|
|
|
- List<Object> values = this.template().executePipelined(new SessionCallback<Object>() {
|
|
|
- @Override
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
|
|
- operations.execute(ADD_SCRIPT, (List<K>) keys, args);
|
|
|
- operations.opsForZSet().removeRangeByScore((K) name, 0, max);
|
|
|
- return null;
|
|
|
- }
|
|
|
- });
|
|
|
- count = ObjectUtils.isEmpty(values) ? null : (Long) values.get(0);
|
|
|
- } else {
|
|
|
- count = this.template().execute(ADD_SCRIPT, this.keys, args);
|
|
|
- }
|
|
|
- return count != null && count > 0;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取RedisTemplate实例
|
|
|
- *
|
|
|
- * @return RedisTemplate实例
|
|
|
- */
|
|
|
- protected RedisTemplate<String, E> template() {
|
|
|
- return RedisContextHolder.getDefaultTemplate();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 判断元素是否在临时状态
|
|
|
- *
|
|
|
- * @param e 元素对象
|
|
|
- * @return true/false
|
|
|
- */
|
|
|
- public boolean temporally(E e) {
|
|
|
- if (e == null) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- Double score = this.template().opsForZSet().score(this.name, e);
|
|
|
- return score != null && score > System.currentTimeMillis();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 判断元素是否在临时状态
|
|
|
- *
|
|
|
- * @param collection 元素集合
|
|
|
- * @return 元素/是否在临时状态映射表
|
|
|
- */
|
|
|
- public Map<E, Boolean> temporally(Collection<E> collection) {
|
|
|
- if (ObjectUtils.isEmpty(collection)) {
|
|
|
- return Collections.emptyMap();
|
|
|
- }
|
|
|
-
|
|
|
- // 处理单个元素
|
|
|
- if (collection.size() == 1) {
|
|
|
- E e = collection.iterator().next();
|
|
|
- return ImmutableMap.of(e, this.temporally(e));
|
|
|
- }
|
|
|
-
|
|
|
- // 批量处理多个元素
|
|
|
- List<Object> scores = this.template().executePipelined(new SessionCallback<Object>() {
|
|
|
- @Override
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
|
|
- for (E e : collection) {
|
|
|
- operations.opsForZSet().score((K) name, e);
|
|
|
- }
|
|
|
- return null;
|
|
|
- }
|
|
|
- });
|
|
|
- if (ObjectUtils.isEmpty(scores)) {
|
|
|
- return Collections.emptyMap();
|
|
|
- }
|
|
|
- int i = 0;
|
|
|
- long timestamp = System.currentTimeMillis();
|
|
|
- Map<E, Boolean> consumings = Maps.newHashMapWithExpectedSize(collection.size());
|
|
|
- for (E e : collection) {
|
|
|
- Double score = (Double) scores.get(i++);
|
|
|
- consumings.put(e, score != null && score > timestamp);
|
|
|
- }
|
|
|
- return consumings;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public Iterator<E> iterator() {
|
|
|
- return new Iter(this.size());
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int size() {
|
|
|
- Long size = this.template().opsForZSet().size(this.name);
|
|
|
- return size == null ? 0 : size.intValue();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean add(E e) {
|
|
|
- return this.offer(e);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean offer(E e) {
|
|
|
- return this.add(new Object[]{System.currentTimeMillis(), Objects.requireNonNull(e)});
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- public E poll() {
|
|
|
- long max = System.currentTimeMillis();
|
|
|
- long min = this.timeout > 0 ? max - this.timeout : 0, score = max + this.idle;
|
|
|
- List<E> values = this.template().execute(POLL_SCRIPT, this.keys, min, max, score);
|
|
|
- return ObjectUtils.isEmpty(values) ? null : values.get(0);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public E peek() {
|
|
|
- long min = 0, max = System.currentTimeMillis();
|
|
|
- Set<E> values = this.template().opsForZSet().rangeByScore(this.name, min, max, 0, 1);
|
|
|
- return ObjectUtils.isEmpty(values) ? null : values.iterator().next();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void clear() {
|
|
|
- this.template().delete(this.name);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean addAll(Collection<? extends E> c) {
|
|
|
- if (ObjectUtils.isEmpty(c)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- int i = 0;
|
|
|
- Object[] args = new Object[c.size() * 2];
|
|
|
- long timestamp = System.currentTimeMillis();
|
|
|
- for (E e : c) {
|
|
|
- args[i++] = timestamp++;
|
|
|
- args[i++] = Objects.requireNonNull(e);
|
|
|
- }
|
|
|
- return this.add(args);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean contains(Object o) {
|
|
|
- if (o == null) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- Long index = this.template().opsForZSet().rank(this.name, o);
|
|
|
- return index != null && index >= 0;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean containsAll(Collection<?> c) {
|
|
|
- if (ObjectUtils.isEmpty(c)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // 处理单个元素
|
|
|
- if (c.size() == 1) {
|
|
|
- return this.contains(c.iterator().next());
|
|
|
- }
|
|
|
-
|
|
|
- // 批量处理多个元素
|
|
|
- List<Object> indexes = this.template().executePipelined(new SessionCallback<Object>() {
|
|
|
- @Override
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
|
|
- for (Object o : c) {
|
|
|
- operations.opsForZSet().rank((K) name, o);
|
|
|
- }
|
|
|
- return null;
|
|
|
- }
|
|
|
- });
|
|
|
- return ObjectUtils.notEmpty(indexes) && indexes.size() == c.size()
|
|
|
- && indexes.stream().allMatch(index -> index != null && ((Long) index) >= 0);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean remove(Object o) {
|
|
|
- if (o == null) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- Long count = this.template().opsForZSet().remove(this.name, o);
|
|
|
- return count != null && count > 0;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean removeAll(Collection<?> c) {
|
|
|
- if (ObjectUtils.isEmpty(c)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- Long count = this.template().opsForZSet().remove(this.name, c.toArray());
|
|
|
- return count != null && count > 0;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public Object[] toArray() {
|
|
|
- Set<E> values = this.template().opsForZSet().range(this.name, 0, -1);
|
|
|
- return ObjectUtils.ifNull(values, Collections::emptySet).toArray();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public <T> T[] toArray(T[] a) {
|
|
|
- Set<E> values = this.template().opsForZSet().range(this.name, 0, -1);
|
|
|
- return ObjectUtils.ifNull(values, Collections::emptySet).toArray(a);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public String toString() {
|
|
|
- Set<E> values = this.template().opsForZSet().range(this.name, 0, -1);
|
|
|
- return ObjectUtils.ifNull(values, Collections::emptySet).toString();
|
|
|
- }
|
|
|
-}
|