当前位置: 首页 > news >正文

网站建设推广工资/百度平台官网

网站建设推广工资,百度平台官网,做网站用什么软件免费,如何利用淘宝建设网站挣钱下面是我们的秒杀流程: 对于正常的秒杀处理,我们需要多次查询数据库,会给数据库造成相当大的压力,这个时候我们需要加入缓存,进而缓解数据库压力。 在上面的图示中,我们可以将一条流水线的任务拆成两条流水…

下面是我们的秒杀流程:

对于正常的秒杀处理,我们需要多次查询数据库,会给数据库造成相当大的压力,这个时候我们需要加入缓存,进而缓解数据库压力。

在上面的图示中,我们可以将一条流水线的任务拆成两条流水线来做,如果我们直接将判断秒杀库存与校验一人一单放在流水线A上,剩下的放在另一条流水线B,那么如果流水线A就可以相当于服务员直接判断是否符合资格,如果符合资格那么直接生成信息给另一条流水线B去处理业务,这里的流水线就是咱们的线程,而流水线A也是基于数据库进行查询,也会压力数据库,那么这种情况我们就可以将待查询信息保存在Redis缓存中。

但是我们不能再流水线A判断完成后去直接调用流水线B,这样的效率是大打折扣的,这种情况我们需要开启独立线程去执行流水线B的操作,如何知道给哪个用户创建订单呢?这个时候就要流水线A在判断成功后去生成信息给独立线程

最后的业务就变成,用户直接访问流水线A,通过流水线A去判断,如果通过则生成信息给流水线B去创建订单,过程如下图:

那么什么样的数据结构满足下面条件:① 一个key能够保存很多值   ②唯一性:一人一单需要保证用户id不能重复。

所以我们需要使用set:

那么如何判断校验用户的购买资格呢?

 而上述判断需要保证原子性,所以我们需要使用Lua脚本进行编写:

local voucherId = ARGV[1]; -- 优惠劵id
local userId = ARGV[2]; -- 用户id-- 库存key
local stockKey = 'seckill:stock' .. voucherId; -- 拼接
-- 订单key
local stockKey = 'seckill:stock' .. voucherId; -- 拼接
-- 判断库存是否充足
if(tonumber(redis.call('get',stockKey) <= 0)) then-- 库存不足,返回1return 1;
end;
-- 判断用户是否下单
if(redis.call('sismember',orderKey,userId)) then-- 存在,说明重复下单,返回2return 2;
end
-- 扣减库存 incrby stockKey -1
redis.call('incrby',stockKey,-1);
-- 下单(保存用户) sadd orderKey userId
redis.call('sadd',orderKey,userId);
return 0;

之后我们按照下面步骤来实现代码:

在方法体内执行Lua脚本来原子性判断,然后判断是否能够处理并传入阻塞队列:

@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT; // 泛型内填入返回值类型static { // 静态属性要使用静态代码块进行初始化SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setResultType(Long.class);SECKILL_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));}public Result seckillVoucherMax(Long voucherId) {// 获取用户信息Long userId = UserHolder.getUser().getId();// 1.执行Lua脚本来判断用户资格Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(), // Lua无需接受keyvoucherId.toString(),userId.toString());// 2.判断结果是否为0int r = result.intValue();if(r != 0) {// 不为0代表无资格购买return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}// 3.有购买资格则将下单信息保存到阻塞队列中// ... return Result.ok();}}

 接下来我们创建阻塞队列,线程池以及线程方法,随后使用Springboot提供的注解在@PostConstruct去给线程池传入线程方法:

@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT; // 泛型内填入返回值类型static { // 静态属性要使用静态代码块进行初始化SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setResultType(Long.class);SECKILL_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));}private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024); // 创建阻塞队列private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();  // 创建线程池// 让大类在开始初始化时就能够执行线程任务@PostConstructprivate void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderTask());}// 创建线程任务private class VoucherOrderTask implements Runnable {@Overridepublic void run() {while(true){try {// 获取队列中的订单信息VoucherOrder voucherOrder = orderTasks.take();// 取出头部信息// 创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常",e);}}}}// 创建订单private void handleVoucherOrder(VoucherOrder voucherOrder) {RLock lock = redissonClient.getLock("lock:order:" + voucherOrder.getUserId().toString());boolean isLock = lock.tryLock();// 判断是否获取锁成功if (!isLock) {// 获取锁失败,返回错误或重试log.error("不允许重复下单");return ;}try {proxy.createVoucherOrderMax(voucherOrder);} finally {lock.unlock();}}@Overridepublic void createVoucherOrderMax(VoucherOrder voucherOrder) {// 一人一单Long userId = voucherOrder.getUserId();// 查询订单int count = query().eq("user_id",userId).eq("voucher_id", voucherOrder.getVoucherId()).count();// 判断是否存在if(count > 0){// 用户已经购买过log.error("用户已经购买过");return ;}// CAS改进:将库存判断改成stock > 0以此来提高性能boolean success = seckillVoucherService.update().setSql("stock= stock -1") // set stock = stock - 1.eq("voucher_id", voucherOrder.getVoucherId()).eq("stock",0) // where id = ? and stock > 0.update();if (!success) {//扣减库存log.error("库存不足!");return ;}//6.创建订单save(voucherOrder);}private IVoucherOrderService proxy; // 代理对象public Result seckillVoucherMax(Long voucherId) {// 获取用户信息Long userId = UserHolder.getUser().getId();// 1.执行Lua脚本来判断用户资格Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(), // Lua无需接受keyvoucherId.toString(),userId.toString());// 2.判断结果是否为0int r = result.intValue();if(r != 0) {// 不为0代表无资格购买return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}// 3.有购买资格则将下单信息保存到阻塞队列中Long orderId = redisIdWorker.nextId("order");// 创建订单VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setId(orderId);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);// 放入阻塞队列orderTasks.add(voucherOrder);// 4.获取代理对象(线程异步执行,需要手动在方法内获取)proxy = (IVoucherOrderService)AopContext.currentProxy(); // 获取当前类的代理对象  (需要引入aspectjweaver依赖,并且在实现类加入@EnableAspectJAutoProxy(exposeProxy = true)以此来暴露代理对象)return Result.ok();}}

在上面代码中,我们使用下面代码创建了一个单线程的线程池。它保证所有提交的任务都按照提交的顺序执行,每次只有一个线程在工作。

private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

下面代码是一个常见的阻塞队列实现,具有固定大小(在这里是 1024 * 1024),它的作用是缓冲和排队任务。ArrayBlockingQueue 是一个线程安全的队列,它会自动处理线程之间的同步问题。当队列满时,调用 put() 方法的线程会被阻塞,直到队列有空间;当队列为空时,调用 take() 方法的线程会被阻塞,直到队列中有数据。

private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);

在下面代码中,orderTasks 阻塞队列用于存放需要处理的订单对象,每个订单的处理逻辑都由 VoucherOrderTask 线程池中的线程异步执行:

VoucherOrder voucherOrder = orderTasks.take();
handleVoucherOrder(voucherOrder);

之后我们需要调用 Runnable 接口去实现VoucherOrderTask类以此来创建线程方法

private class VoucherOrderTask implements Runnable {@Overridepublic void run() {while (true) {try {// 获取队列中的订单信息VoucherOrder voucherOrder = orderTasks.take(); // 获取订单// 创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常", e);}}}
}

随后将线程方法通过 submit() 方法将 VoucherOrderTask 提交到线程池中,这个任务是一个无限循环的任务,它会不断从阻塞队列中取出订单并处理,直到线程池关闭。这种方式使得订单处理任务可以异步执行,而不阻塞主线程,提高了系统的响应能力:

@PostConstruct
private void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderTask());
}

但是在高并发的情况下就会产生大量订单,就会超出JVM阻塞队列的上线,并且每当服务重启或者宕机的情况发生,阻塞队列的所有订单任务就都会丢失。

所以为了解决这种情况,我们就要使用消息队列去解决这个问题:


什么是消息队列?


消息队列(Message Queue, MQ)是一种用于在应用程序之间传递消息的通信方式。它允许应用程序通过发送和接收消息来解耦,从而提高系统的可扩展性、可靠性和灵活性。消息队列通常用于异步通信、任务队列、事件驱动架构等场景。

消息队列的核心概念 :

  1. 生产者(Producer):发送消息到消息队列的应用程序。

  2. 消费者(Consumer):从消息队列中接收并处理消息的应用程序。

  3. 队列(Queue):消息的存储区域,生产者将消息发送到队列,消费者从队列中获取消息。

  4. 消息(Message):在生产者与消费者之间传递的数据单元。

  5. Broker:消息队列的服务器,负责接收、存储和转发消息。

消息队列是在JVM以外的一个独立的服务,能够不受JVM内存的限制,并且存入MQ的信息都可以做持久化存储。

详细教学可以查询下面链接:微服务架构 --- 使用RabbitMQ进行异步处理 


但是这样的方式是需要额外提供服务的,所以我们可以使用Redis提供的三种不同的方式来实现消息队列

  1. List 结构实现消息队列

  2. Pub/Sub(发布/订阅)模式

  3. Stream 结构(Redis 5.0 及以上版本)(推荐使用)(详细介绍)


使用 List 结构实现消息队列:

Redis 的 List 数据结构是一个双向链表,支持从头部或尾部插入和弹出元素。我们可以利用 LPUSH 和 BRPOP 命令实现一个简单的消息队列。

实现步骤:

  • 生产者:使用 LPUSH 将消息推入队列。

  • 消费者:使用 BRPOP 阻塞地从队列中获取消息。

生产者代码:

import redis.clients.jedis.Jedis;public class ListProducer {public static void main(String[] args) {Jedis jedis = new Jedis("localhost", 6379); // 连接 RedisString queueName = "myQueue";// 发送消息for (int i = 1; i <= 5; i++) {String message = "Message " + i;jedis.lpush(queueName, message); // 将消息推入队列System.out.println("Sent: " + message);}jedis.close(); // 关闭连接}
}

消费者代码:

import redis.clients.jedis.Jedis;public class ListConsumer {public static void main(String[] args) {Jedis jedis = new Jedis("localhost", 6379); // 连接 RedisString queueName = "myQueue";while (true) {// 阻塞获取消息,超时时间为 0(无限等待)var result = jedis.brpop(0, queueName);String message = result.get(1); // 获取消息内容System.out.println("Received: " + message);}}
}
  • 优点:简单易用,适合轻量级场景。

  • 缺点不支持消息确认机制,消息一旦被消费(从队列内取出)就会从队列中删除。并且只支持单消费者(一个消息只能拿出一次)


使用 Pub/Sub 模式实现消息队列: 

Redis 的 Pub/Sub 模式是一种发布-订阅模型,生产者将消息发布到频道,消费者订阅频道以接收消息。

实现步骤:

  • 生产者:使用 PUBLISH 命令向频道发布消息。

  • 消费者:使用 SUBSCRIBE 命令订阅频道。

生产者代码:

import redis.clients.jedis.Jedis;public class PubSubProducer {public static void main(String[] args) {Jedis jedis = new Jedis("localhost", 6379); // 连接 RedisString channelName = "myChannel";// 发布消息for (int i = 1; i <= 5; i++) {String message = "Message " + i;jedis.publish(channelName, message); // 发布消息到频道System.out.println("Published: " + message);}jedis.close(); // 关闭连接}
}

 消费者代码:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;public class PubSubConsumer {public static void main(String[] args) {Jedis jedis = new Jedis("localhost", 6379); // 连接 RedisString channelName = "myChannel";// 创建订阅者JedisPubSub subscriber = new JedisPubSub() {@Overridepublic void onMessage(String channel, String message) {System.out.println("Received: " + message);}};// 订阅频道jedis.subscribe(subscriber, channelName);}
}
  • 优点:支持一对多的消息广播。

  • 缺点:消息是即时的,如果消费者不在线,消息会丢失。


但是上面两方式都是有缺点的:

  1. 不支持消息确认机制,消息一旦被消费(从队列内取出)就会从队列中删除。并且只支持单消费者(一个消息只能拿出一次)
  2. 消息是即时的,如果消费者不在线,消息会丢失。

所以根据上面的两种方式,我们推出一款全新的方式 ->

使用 Stream 结构实现消息队列:

Redis Stream 是一种强大的数据结构,用于管理消息流。它将消息存储在 Redis 中,并允许消费者按顺序获取消息。Stream 具有以下特点:

  • 有序消息:消息按插入顺序排列。
  • 消费者组:一个消费者组可以有多个消费者,每个消费者可以独立消费不同的消息。
  • 消息 ID:每条消息都有唯一的 ID(如:1588890470850-0),ID 按时间戳生成。
  • 自动分配消息:多个消费者可以从 Stream 中并行消费消息,保证消息不会重复消费。

在 Redis Stream 中,一个队列可以有多个消费者组,每个消费者组可以独立地消费队列中的消息。每个消费者组内有多个消费者,而消费者是基于 消费者名称 进行识别的。 

消费者组的工作方式:

  • 每个消费者组拥有自己的 消费进度,也就是每个消费者组会从 自己独立的消息 ID 开始消费
  • 多个消费者组之间是相互独立的,即使它们消费的是同一个队列,它们也可以从不同的位置开始消费队列中的消息。
  • 每个消费者组都可以有多个 消费者(在同一个组内,多个消费者可以并行消费同一个队列的消息,但每个消息在消费者组内只能被一个消费者处理一次)。

假设有一个队列(Stream)mystream,可以为它创建多个消费者组:

XGROUP CREATE mystream group1 $ MKSTREAM
XGROUP CREATE mystream group2 $ MKSTREAM

这样,mystream 队列上就有了两个消费者组:group1group2。每个消费者组可以有自己的消费者并从该队列中读取消息。此时,group1group2 都在消费同一个队列 mystream,但它们的消费进度是独立的,它们各自有自己的消息 ID 记录。

每个消费者组可以有多个消费者,而每个消费者通过一个 唯一的消费者名称 来标识。


每个消费者组有独立的消费进度


每个消费者组会记录自己的消费进度,也就是它消费到队列中的 哪个消息 ID。即使多个消费者组在消费同一个消息队列,它们每个组都会从 不同的消费位置(消息 ID)开始读取消息。

例如,假设有一个队列 mystream,同时有两个消费者组 group1group2,它们都从 mystream 队列中读取消息:

  • group1mystream 队列中的消息 id1 开始消费,group1 的进度会记录在 Redis 中。
  • group2mystream 队列中的消息 id2 开始消费,group2 的进度也会记录在 Redis 中。

消费进度互不干扰,即便 group1group2 都在消费 mystream 队列,它们的消费位置是独立的。


消费者组内部的消息消费


一个消费者组内的消费者会 共享 组内的消息。即使有多个消费者,每条消息 在消费者组内部只会被 一个消费者 消费。消费者之间会并行处理消息,但每条消息只会被一个消费者处理。

举个例子:假设 group1 中有三个消费者 consumer1consumer2consumer3,如果队列 mystream 有 6 条消息,那么它们会如下消费:

  • consumer1 处理消息 12
  • consumer2 处理消息 34
  • consumer3 处理消息 56

但对于消费者组 group2,如果它有自己的消费者,group2 内的消费者也会并行消费 mystream 中的消息,而 group1group2 之间没有直接关系。

首先初始化一个消息队列:

在项目启动时,确保 Redis 中存在对应的 Stream 和消费者组。可以通过程序在启动时检查并创建(如果不存在的话)。

@Configuration
public class RedisStreamConfig {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String STREAM_KEY = "mystream";private static final String GROUP_NAME = "mygroup";@PostConstructpublic void init() {// 检查消费者组是否存在,若不存在则创建try {// 如果消费者组不存在则会抛出异常,我们捕获异常进行创建redisTemplate.opsForStream().groups(STREAM_KEY);} catch (Exception e) {// 创建消费者组,起始位置为 $ 表示从末尾开始消费新消息redisTemplate.opsForStream().createGroup(STREAM_KEY, GROUP_NAME);}}
}

注意:

  • opsForStream().groups(STREAM_KEY):查询消费者组是否已存在。
  • opsForStream().createGroup(STREAM_KEY, GROUP_NAME):如果没有消费者组,则创建一个新的组。

随后我们生产者发送消息示例:

@Service  
public class RedisStreamProducerService {  // 定义生产者服务类 RedisStreamProducerServiceprivate static final String STREAM_KEY = "mystream";  // 定义 Redis Stream 的名称,这里指定队列名为 "mystream"@Autowired  private StringRedisTemplate redisTemplate;public void sendMessage(String content) {  // 定义一个方法,发送消息到 Redis Stream,参数 content 是消息的内容Map<String, String> map = new HashMap<>();  // 创建一个 Map 用来存储消息内容map.put("content", content);  // 将消息内容添加到 Map 中,键是 "content",值是传入的内容// 在消息队列中添加消息,调用 StringRedisTemplate 的 opsForStream 方法RecordId recordId = redisTemplate.opsForStream()  // 获取操作 Redis Stream 的操作对象.add(StreamRecords.objectBacked(map)  // 创建一个 Stream 记录,将 Map 转化为对象记录.withStreamKey(STREAM_KEY));  // 设置该记录属于的 Stream(消息队列)的名称// 输出记录的 ID,表示消息已经成功发送System.out.println("消息发送成功,id: " + recordId.getValue());  // 打印消息的 ID,表明该消息已经被成功加入到 Stream 中}
}

RecordId 是 Spring Data Redis 中的一个类,用来表示 消息的唯一标识符。它对应 Redis Stream 中的 消息 ID,该 ID 是 Redis Stream 中每条消息的唯一标识。Redis 中的消息 ID 通常是由时间戳和序号组成的(如 1588890470850-0)。

主要功能:
  • 表示消息 IDRecordId 是一个封装类,表示 Redis Stream 中消息的 ID。
  • 用于识别和操作消息:在消费和确认消息时,RecordId 用来标识每条消息的唯一性,并帮助 Redis 确定消息是否已经被消费
使用场景:

RecordId 用来标识从 Stream 中读取到的消息,我们可以通过 RecordId 来进行消息的确认、删除或其他操作。

RecordId recordId = redisTemplate.opsForStream().add(StreamRecords.objectBacked(map).withStreamKey("mystream"));

通过 StreamRecords.objectBacked(map)map 对象作为消息内容,并用 add 方法将其写入 Stream。

在然后编写消费者服务:

使用 RedisTemplate 的 read 方法(底层执行的是 XREADGROUP 命令)从消费者组中拉取消息,并进行处理。消费者可以采用定时任务或后台线程不断轮询

@Slf4j  
@Service  
public class RedisStreamConsumerService { private static final String STREAM_KEY = "mystream";  // Redis Stream 的名称,这里指定队列名为 "mystream"private static final String GROUP_NAME = "mygroup";  // 消费者组的名称,多个消费者可以通过组名共享消费队列private static final String CONSUMER_NAME = "consumer-1";  // 消费者的名称,消费者名称在同一消费者组内必须唯一@Autowired  private StringRedisTemplate redisTemplate;@PostConstruct  // 使用该注解能让方法在 Spring 完成依赖注入后自动调用,用于初始化任务@Async  // 将该方法标记为异步执行,允许它在单独的线程中运行,不会阻塞主线程,@EnableAsync 需要在配置类中启用public void start() {  // 启动方法,在应用启动时执行// 无限循环,不断从 Redis Stream 中读取消息(可以改为定时任务等方式)while (true) {try {// 设置 Stream 读取的阻塞超时,设置最多等待 2 秒StreamReadOptions options = StreamReadOptions.empty().block(Duration.ofSeconds(2));// 从指定的消费者组中读取消息,">" 表示只消费未被消费过的消息List<MapRecord<String, Object, Object>> messages = redisTemplate.opsForStream().read(Consumer.from(GROUP_NAME, CONSUMER_NAME),  // 指定消费者组和消费者名称options,  // 设置读取选项,包含阻塞时间StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed())  // 从最后消费的消息开始读取);// 如果没有消息,继续循环读取if (messages == null || messages.isEmpty()) {continue;  }// 处理每一条读取到的消息for (MapRecord<String, Object, Object> message : messages) {String messageId = message.getId();  // 获取消息的唯一标识符(ID)Map<Object, Object> value = message.getValue();  // 获取消息内容(以 Map 形式存储)log.info("接收到消息,id={},内容={}", messageId, value);  // 打印日志,记录消息 ID 和内容// 在这里加入业务逻辑处理// 例如处理消息并执行相应的操作// ...// 消息处理成功后,需要确认消息已经被消费(通过 XACK 命令)redisTemplate.opsForStream().acknowledge(STREAM_KEY, GROUP_NAME, messageId);  // 确认消费的消息}} catch (Exception e) {log.error("读取 Redis Stream 消息异常", e);  // 异常捕获,记录错误日志}}}
}

MapRecord<String, Object, Object> 是 Spring Data Redis 用来表示 Redis Stream 中的 消息记录 的类。它不仅包含了消息的 ID,还包含了消息的内容(即消息数据)。在 Redis 中,每条消息都存储为一个 key-value 对。

主要功能:
  • 封装消息 ID 和消息内容MapRecord 用来封装消息的 ID 和消息的内容。
  • 消息的内容:消息的内容通常是一个 键值对Map<String, Object>),可以是任意对象的数据结构(例如,JSON、Map 或其他序列化对象)。
字段:
  • getId():返回消息的 ID(RecordId 类型)。
  • getValue():返回消息的内容,以 Map<Object, Object> 的形式。
使用场景:

MapRecord 是用来表示从 Stream 中读取到的消息,它将消息的 ID 和内容(键值对)封装在一起。你可以使用 MapRecord 来获取消息的 ID 和内容并处理。

MapRecord<String, Object, Object> message = redisTemplate.opsForStream().read(Consumer.from("mygroup", "consumer1"), options, StreamOffset.create("mystream", ReadOffset.lastConsumed()));

在这个例子中,message 是一个 MapRecord 实例,它封装了从 mystream 队列中读取到的消息。我们可以通过 message.getId() 获取消息 ID,通过 message.getValue() 获取消息内容。

在消费者中,我们使用 MapRecord<String, Object, Object> 来封装消息,获取 message.getId() 来获取消息的 ID(RecordId),以及通过 message.getValue() 获取消息的内容。 随后在处理完消息后,调用 acknowledge() 来确认消息已经被消费。

最后启动异步支持:

@SpringBootApplication
@EnableAsync // 启动异步支持
public class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class, args);}
}

通过这种方式,Spring Data Redis 提供了高效且类型安全的接口来操作 Redis Stream,帮助我们在分布式系统中实现高效的消息队列。

相关文章:

Redis --- 秒杀优化方案(阻塞队列+基于Stream流的消息队列)

下面是我们的秒杀流程&#xff1a; 对于正常的秒杀处理&#xff0c;我们需要多次查询数据库&#xff0c;会给数据库造成相当大的压力&#xff0c;这个时候我们需要加入缓存&#xff0c;进而缓解数据库压力。 在上面的图示中&#xff0c;我们可以将一条流水线的任务拆成两条流水…...

如何确认设备文件 /dev/fb0 对应的帧缓冲设备是开发板上的LCD屏?如何查看LCD屏的属性信息?

要判断 /dev/fb0 是否对应的是 LCD 屏幕&#xff0c;可以通过以下几种方法&#xff1a; 方法 1&#xff1a;使用 fbset 命令查看帧缓冲设备的属性信息 Linux 的 帧缓冲设备&#xff08;Framebuffer&#xff09; 通常在 /dev/fbX 下&#xff0c;/dev/fb0 一般是主屏幕&#xff…...

C++多线程编程——基于策略模式、单例模式和简单工厂模式的可扩展智能析构线程

1. thread对象的析构问题 在 C 多线程标准库中&#xff0c;创建 thread 对象后&#xff0c;必须在对象析构前决定是 detach 还是 join。若在 thread 对象销毁时仍未做出决策&#xff0c;程序将会终止。 然而&#xff0c;在创建 thread 对象后、调用 join 前的代码中&#xff…...

AI与SEO关键词的完美结合如何提升网站流量与排名策略

内容概要 在当今数字营销环境中&#xff0c;内容的成功不仅依赖于高质量的创作&#xff0c;还包括高效的关键词策略。AI与SEO关键词的结合&#xff0c;正是这一趋势的重要体现。 AI技术在SEO中的重要性 在数字营销领域&#xff0c;AI技术的引入为SEO策略带来了前所未有的变革。…...

保姆级教程Docker部署Kafka官方镜像

目录 一、安装Docker及可视化工具 二、单节点部署 1、创建挂载目录 2、运行Kafka容器 3、Compose运行Kafka容器 4、查看Kafka运行状态 三、集群部署 在Kafka2.8版本之前&#xff0c;Kafka是强依赖于Zookeeper中间件的&#xff0c;这本身就很不合理&#xff0c;中间件依赖…...

解析PHP文件路径相关常量

PHP文件路径相关常量包括以下几个常量&#xff1a; __FILE__&#xff1a;表示当前文件的绝对路径&#xff0c;包括文件名。 __DIR__&#xff1a;表示当前文件所在的目录的绝对路径&#xff0c;不包括文件名。 dirname(__FILE__)&#xff1a;等同于__DIR__&#xff0c;表示当前…...

WPS计算机二级•幻灯片的配色、美化与动画

听说这是目录哦 配色基础颜色语言❤️使用配色方案&#x1fa77;更改PPT的颜色&#x1f9e1;PPT动画添加的原则&#x1f49b;PPT绘图工具&#x1f49a;自定义设置动画&#x1f499;使用动画刷复制动画效果&#x1fa75;制作文字打字机效果&#x1f49c;能量站&#x1f61a; 配色…...

C#,shell32 + 调用控制面板项(.Cpl)实现“新建快捷方式对话框”(全网首发)

Made By 于子轩&#xff0c;2025.2.2 不管是使用System.IO命名空间下的File类来创建快捷方式文件&#xff0c;或是使用Windows Script Host对象创建快捷方式&#xff0c;亦或是使用Shell32对象创建快捷方式&#xff0c;都对用户很不友好&#xff0c;今天小编为大家带来一种全新…...

单纯信息展示的站点是否可以用UML建模

凌钦亮 More options Aug 7 2010, 10:36 am 现在社会上大量的网站需求都还只是用于单纯的企业信息展示&#xff0c;那此种网站是否有必要用UML 建模呢&#xff1f;业务用例图的一个个用例是用来卖的&#xff0c;但是他只有信息展示这个需 求&#xff0c;我是否在划分业务用例…...

FinRobot:一个使用大型语言模型的金融应用开源AI代理平台

“FinRobot: An Open-Source AI Agent Platform for Financial Applications using Large Language Models” 论文地址&#xff1a;https://arxiv.org/pdf/2405.14767 Github地址&#xff1a;https://github.com/AI4Finance-Foundation/FinRobot 摘要 在金融领域与AI社区间&a…...

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.19 线性代数核武器:BLAS/LAPACK深度集成

2.19 线性代数核武器&#xff1a;BLAS/LAPACK深度集成 目录 #mermaid-svg-yVixkwXWUEZuu02L {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-yVixkwXWUEZuu02L .error-icon{fill:#552222;}#mermaid-svg-yVixkwXWUEZ…...

开发板目录 /usr/lib/fonts/ 中的字体文件 msyh.ttc 的介绍【微软雅黑(Microsoft YaHei)】

本文是博文 https://blog.csdn.net/wenhao_ir/article/details/145433648 的延伸扩展。 本文是博文 https://blog.csdn.net/wenhao_ir/article/details/145433648 的延伸扩展。 问&#xff1a;运行 ls /usr/lib/fonts/ 发现有一个名叫 msyh.ttc 的字体文件&#xff0c;能介绍…...

Love Tester:探索爱情的深度与维度

爱情是什么&#xff1f;是相互帮助、耐心、理解、鼓励、保护、可靠和牺牲。Love Tester 通过先进的算法&#xff0c;分析名字的兼容性和关系的潜力&#xff0c;帮助你计算爱情匹配的准确性。 相互帮助&#xff1a;在伴侣遇到困难时伸出援手&#xff0c;这是爱情的体现。耐心&a…...

BFS(广度优先搜索)——搜索算法

BFS&#xff0c;也就是广度&#xff08;宽度&#xff09;优先搜索&#xff0c;二叉树的层序遍历就是一个BFS的过程。而前、中、后序遍历则是DFS&#xff08;深度优先搜索&#xff09;。从字面意思也很好理解&#xff0c;DFS就是一条路走到黑&#xff0c;BFS则是一层一层地展开。…...

JVM 四虚拟机栈

虚拟机栈出现的背景 由于跨平台性的设计&#xff0c;Java的指令都是根据栈来设计的。不同平台CPU架构不同&#xff0c;所以不能设计为基于寄存器的。优点是跨平台&#xff0c;指令集小&#xff0c;编译器容易实现&#xff0c;缺点是性能下降&#xff0c;实现同样的功能需要更多…...

【R语言】获取数据

R语言自带2种数据存储格式&#xff1a;*.RData和*.rds。 这两者的区别是&#xff1a;前者既可以存储数据&#xff0c;也可以存储当前工作空间中的所有变量&#xff0c;属于非标准化存储&#xff1b;后者仅用于存储单个R对象&#xff0c;且存储时可以创建标准化档案&#xff0c…...

Java BIO详解

一、简介 1.1 BIO概述 BIO&#xff08;Blocking I/O&#xff09;&#xff0c;即同步阻塞IO&#xff08;传统IO&#xff09;。 BIO 全称是 Blocking IO&#xff0c;同步阻塞式IO&#xff0c;是JDK1.4之前的传统IO模型&#xff0c;就是传统的 java.io 包下面的代码实现。 服务…...

统计满足条件的4位数(信息学奥赛一本通-1077)

【题目描述】 给定若干个四位数&#xff0c;求出其中满足以下条件的数的个数&#xff1a;个位数上的数字减去千位数上的数字&#xff0c;再减去百位数上的数字&#xff0c;再减去十位数上的数字的结果大于零。 【输入】 输入为两行&#xff0c;第一行为四位数的个数n&#xff0…...

北京门头沟区房屋轮廓shp的arcgis数据建筑物轮廓无偏移坐标测评

在IT行业中&#xff0c;地理信息系统&#xff08;GIS&#xff09;是用于处理、分析和展示地理空间数据的重要工具&#xff0c;而ArcGIS则是GIS领域中的一款知名软件。本文将详细解析标题和描述中提及的知识点&#xff0c;并结合“门头沟区建筑物数据”这一标签&#xff0c;深入…...

Spring 面试题【每日20道】【其三】

1、Spring 中的 Profile 注解的作用是什么&#xff1f; 中等 Profile 注解在Spring框架中用于根据不同的环境配置文件&#xff08;profiles&#xff09;来激活或忽略某些Bean的注册。它允许开发者定义逻辑以区分不同环境下的bean定义&#xff0c;例如开发、测试和生产环境。 …...

FFmpeg(7.1版本)在Ubuntu18.04上的编译

一、从官网上下载FFmpeg源码 官网地址:Download FFmpeg 点击Download Source Code 下载源码到本地电脑上 二、解压包 tar -xvf ffmpeg-7.1.tar.xz 三、配置configure 1.准备工作 安装编译支持的软件 ① sudo apt-get install nasm //常用的汇编器,用于编译某些需要汇编…...

Apache Hudi数据湖技术应用在网络打车系统中的系统架构设计、软硬件配置、软件技术栈、具体实现流程和关键代码

网络打车系统利用Hudi数据湖技术成功地解决了其大规模数据处理和分析的难题&#xff0c;提高了数据处理效率和准确性&#xff0c;为公司的业务发展提供了有力的支持。 Apache Hudi数据湖技术的一个典型应用案例是网络打车系统的数据处理场景&#xff0c;具体如下&#xff1a; 大…...

安全策略配置

需求: 1、VLAN 2属于办公区;VLAN 3属于生产区 2、办公区PC在工作日时间(周一至周五&#xff0c;早8到晚6)可以正常访问0A Server&#xff0c;其他时间不允许 3、办公区PC可以在任意时刻访问web server 4、生产区PC可以在任意时刻访问0A Server&#xff0c;但是不能访问Web serv…...

c++ stl 遍历算法和查找算法

概述&#xff1a; 算法主要由头文件<algorithm> <functional> <numeric> 提供 <algorithm> 是所有 STL 头文件中最大的一个&#xff0c;提供了超过 90 个支持各种各样算法的函数&#xff0c;包括排序、合并、搜索、去重、分解、遍历、数值交换、拷贝和…...

【Envi遥感图像处理】008:波段(批量)分离与波段合成

文章目录 一、波段分离提取1. 提取单个波段2. 批量提取单个波段二、波段合成相关阅读:【ArcGIS微课1000例】0058:波段合成(CompositeBands)工具的使用 一、波段分离提取 1. 提取单个波段...

线程创建与管理 - 创建线程、线程同步(C++)

前言 在现代软件开发中&#xff0c;线程的创建和管理是并发编程的核心内容之一。通过合理地创建和管理线程&#xff0c;可以有效提高程序的响应速度和资源利用率。本文将详细讲解如何在C中创建线程&#xff0c;并探讨几种常见的线程同步机制。我们假设读者具备一定的C基础&…...

【C语言篇】“三子棋”

一、游戏介绍 三子棋&#xff0c;英文名为 Tic - Tac - Toe&#xff0c;是一款简单而经典的棋类游戏。游戏在一个 33 的棋盘上进行&#xff0c;两名玩家轮流在棋盘的空位上放置自己的棋子&#xff08;通常用 * 和 # 表示&#xff09;&#xff0c;率先在横、竖或斜方向上连成三个…...

安培定律应用于 BH 曲线上的工作点

在本篇博文中&#xff0c;我将展示如何应用安培定律来确定磁芯包裹的导体必须承载多少电流才能从 BH 值工作点获得 B 值&#xff0c;该工作点对应于磁芯材料中的最大 B 值。我在 BH 曲线上使用两个工作点&#xff0c;一个在线性区域&#xff0c;另一个在饱和区域。 安培定律 H…...

深度求索DeepSeek横空出世

真正的强者从来不是无所不能&#xff0c;而是尽我所能。多少有关输赢胜负的缠斗&#xff0c;都是直面本心的搏击。所有令人骄傲振奋的突破和成就&#xff0c;看似云淡风轻寥寥数语&#xff0c;背后都是数不尽的焚膏继晷、汗流浃背。每一次何去何从的困惑&#xff0c;都可能通向…...

【CSS】什么是响应式设计?响应式设计的基本原理,怎么做

在当今多设备、多屏幕尺寸的时代&#xff0c;网页设计面临着前所未有的挑战。传统的固定布局已无法满足用户在不同设备上浏览网页的需求&#xff0c;响应式设计&#xff08;Responsive Web Design&#xff09;应运而生&#xff0c;成为网页设计的趋势和标准。本文将深入探讨响应…...