Spring Boot中借助Redis实现分布式系统全局共享线程安全的阻塞队列
背景问题
我们都知道Java里的LinkedBlockingQueue,采用先进先出(FIFO)的方式存储元素,并且支持同时进行并发的读和写操作。内部使用ReentrantLock锁来保证多线程环境下的线程安全性。
LinkedBlockingQueue提供了以下主要方法:
- put(E e):将元素e插入队列的尾部,如果队列已满则阻塞直到有空间可用。
- offer(E e, long timeout, TimeUnit unit):将元素e插入队列的尾部,如果队列已满则阻塞一段时间,超时后返回false。
- take():获取并移除队列头部的元素,如果队列为空则阻塞直到有元素可用。
- poll(long timeout, TimeUnit unit):获取并移除队列头部的元素,如果队列为空则阻塞一段时间,超时后返回null。
- peek():获取但不移除队列头部的元素,如果队列为空则返回null。
- size():返回队列中的元素数量。
- isEmpty():判断队列是否为空。
LinkedBlockingQueue相比于ConcurrentLinkedQueue,它提供了阻塞操作,使得线程可以等待队列满或者队列空这样的条件,以便更好地控制线程的同步和协作。因此,它更适用于生产者-消费者模式和任务调度等场景。
需要注意的是,LinkedBlockingQueue的容量可以选择是否有限。可以在创建LinkedBlockingQueue对象时指定容量大小,如果不指定则默认为Integer.MAX_VALUE,即无界队列。当队列达到容量限制时,put操作将被阻塞,直到队列中有空间可用;take操作将被阻塞,直到队列中有元素可取。
总结起来,LinkedBlockingQueue是一个线程安全的阻塞队列,适用于多线程并发环境下的任务调度和协作。
LinkedBlockingQueue很好,但是如果我们需要一个分布式系统全局共享的线程安全的阻塞队列,就得换方法实现了,这边我选择的是使用redis的List数据结构实现队列。
redis队列代码
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.BoundListOperations; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.List; import java.util.concurrent.TimeUnit; /** * 夏威夷8080 * @param <T> */ public class RedisBlockingQueue<T> { private String queueKey; private BoundListOperations<String, T> listOps; public RedisBlockingQueue(String queueKey, RedisTemplate<String, T> redisTemplate) { this.queueKey = queueKey; this.listOps = redisTemplate.boundListOps(queueKey); // 设置key和value的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(redisTemplate.getDefaultSerializer()); redisTemplate.afterPropertiesSet(); } public void put(T item) { listOps.leftPush(item); } public T take() { while (true) { // 从队列右侧取出元素,在这里,参数0表示阻塞时间(单位:毫秒)。 // 当队列为空时,如果设置阻塞时间为0,则表示立即返回null,而不进行等待。如果设置的阻塞时间大于0, // 则表示在指定的时间内等待,直到队列中有可弹出的元素或超时。 // //具体解释如下: // //如果队列非空,会立即将左侧的一个元素弹出并返回该元素。 //如果队列为空且阻塞时间为0,那么方法会立即返回null。 //如果队列为空且阻塞时间大于0,那么方法会在指定的时间内等待,直到队列中有可弹出的元素或超时。如果超时时仍没有可弹出的元素,则方法会返回null。 //这边的listOps.rightPop(0)被用于实现一个阻塞的队列操作,即当队列为空时,消费者线程会在此处等待,直到队列中有可弹出的元素或超时。 T item = listOps.rightPop(0, TimeUnit.MINUTES); if (item != null) { return item; } } } public int size() { return Math.toIntExact(listOps.size()); } public boolean isEmpty() { return size() == 0; } }
测试类
三个线程,一个线程模拟生产者推消息,一个线程模拟消费者消费消息,其中消费的速度比生产的速度慢些,模拟队列堆积消息的场景。还有一个线程打印队列实时大小和元素数量。
import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import javax.annotation.PostConstruct; import java.util.List; @RestController @RequestMapping("/test") @Api(value = "测试", tags = "测试redis队列") @Slf4j public class TesterController { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping("/tt") @ApiOperation(value = "测试redis队列") public void tt() { new Thread(new Runnable() { @Override public void run() { // 创建RedisBlockingQueue实例 RedisBlockingQueue<String> blockingQueue = new RedisBlockingQueue<>("myQueue", redisTemplate); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { // 往队列中添加元素 String a = "item" + i; blockingQueue.put(a); log.info("放进元素:" + a); try { Thread.sleep(700); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { // 从队列中取出元素 String item = blockingQueue.take(); log.info("取出的元素:" + item); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { // 获取队列大小和是否为空 log.info("队列大小:" + blockingQueue.size()); log.info("队列是否为空:" + blockingQueue.isEmpty()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }).start(); } }
升级优化为有界队列
Redis的List是一种无界队列,它可以存储任意多个元素而不受限制。所以RedisBlockingQueue并没有固定的最大容量。我们可以根据实际需求在使用时进行控制,例如通过设置最大长度限制,或者在添加元素时进行判断和处理。
下面是一个修改后的RedisBlockingQueue类的示例,通过设置最大长度来限制队列的容量:
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.BoundListOperations; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.List; public class RedisBlockingQueue<T> { private String queueKey; private BoundListOperations<String, T> listOps; private int maxQueueSize; // 最大队列长度 public RedisBlockingQueue(String queueKey, RedisTemplate<String, T> redisTemplate, int maxQueueSize) { this.queueKey = queueKey; this.listOps = redisTemplate.boundListOps(queueKey); this.maxQueueSize = maxQueueSize; // 设置key和value的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(redisTemplate.getDefaultSerializer()); redisTemplate.afterPropertiesSet(); } public void put(T item) throws InterruptedException { while (listOps.size() >= maxQueueSize) { // 队列已满,等待消费者取出元素 Thread.sleep(1000); // 可根据实际情况调整等待时间 } listOps.lefttPush(item); } // 其他方法同上... }
线程安全
leftPop或rightPop
操作,可以实现线程安全的阻塞队列。Redis的leftPop
操作是原子性的,确保每个消费者在获取元素时的互斥性,避免了并发竞争的问题。在示例中,当队列为空时,消费者线程会调用listOps.leftPop(0)
进行阻塞等待。由于Redis的leftPop
操作是原子性的,每次只有一个线程能够成功地弹出队列中的元素。其他线程虽然也在等待,但它们会被阻塞住,直到有元素可供弹出。
因此,这种方式实现的阻塞队列是线程安全的,可以安全地在多个线程之间进行操作,保证了数据的一致性和并发安全性。
BoundListOperations介绍
BoundListOperations
是Spring Data Redis库中的一个接口,它提供了一组用于操作Redis列表(List)数据结构的方法。通过BoundListOperations
接口,我们可以方便地对Redis列表进行各种操作,而无需显式指定列表的键。
BoundListOperations
接口的实例通常通过RedisTemplate.boundListOps(key)
方法来获取,其中RedisTemplate
是Spring Data Redis库提供的Redis操作模板类,key
是Redis列表的键。
BoundListOperations
接口定义了一系列方法,包括:
leftPush(V value)
:将元素从左侧插入列表。rightPush(V value)
:将元素从右侧插入列表。leftPop()
:从左侧弹出列表中的元素。rightPop()
:从右侧弹出列表中的元素。size()
:获取列表的长度。- 等等...
使用BoundListOperations
接口,我们可以更方便地操作Redis列表,而无需每次都指定列表的键。例如,在示例中,通过listOps.rightPush(item)
就可以将元素从右侧插入到Redis列表中。
总之,BoundListOperations
是Spring Data Redis库提供的一个接口,它简化了对Redis列表的操作,并提供了一系列便捷的方法来处理Redis列表。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2018-07-17 使用Linux环境变量教程
2018-07-17 常见的web负载均衡方法总结