springboot+redis 如何实现订单的过期

在Spring Boot中使用Redis实现订单过期(超时取消)功能,有多种成熟方案。以下是完整的实现方案:

一、Redis键过期回调方案(推荐)

1. 配置Redis监听器

java 复制代码
@Configuration
public class RedisKeyExpirationConfig {
    
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}

2. 监听键过期事件

typescript 复制代码
@Component
@Slf4j
public class OrderExpirationListener {
    
    private static final String ORDER_KEY_PREFIX = "order:";
    private static final String ORDER_EXPIRE_KEY_PREFIX = "order:expire:";
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 监听所有键过期事件
     */
    @EventListener
    public void handleKeyExpiredEvent(KeyExpiredEvent<String> event) {
        String expiredKey = new String(event.getSource());
        
        if (expiredKey.startsWith(ORDER_EXPIRE_KEY_PREFIX)) {
            String orderId = expiredKey.substring(ORDER_EXPIRE_KEY_PREFIX.length());
            handleOrderExpired(orderId);
        }
    }
    
    private void handleOrderExpired(String orderId) {
        log.info("检测到订单过期: {}", orderId);
        
        try {
            // 异步处理,避免阻塞Redis监听线程
            CompletableFuture.runAsync(() -> {
                boolean result = orderService.cancelExpiredOrder(orderId);
                if (result) {
                    log.info("订单 {} 已成功取消", orderId);
                } else {
                    log.warn("订单 {} 取消失败或已处理", orderId);
                }
            });
        } catch (Exception e) {
            log.error("处理订单过期异常: {}", orderId, e);
        }
    }
}

3. Redis配置(开启键空间通知)

在Redis配置文件redis.conf中开启键空间通知:

bash 复制代码
# 开启键空间通知
notify-keyspace-events Ex

# 或者开启所有事件
# notify-keyspace-events AKE

Spring Boot配置:

yaml 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    # 监听键过期事件
    listen-patterns: "__keyevent@*__:expired"

二、延时队列方案(Redisson实现)

1. 添加Redisson依赖

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.5</version>
</dependency>

2. 使用Redisson延时队列

typescript 复制代码
@Service
@Slf4j
public class OrderDelayQueueService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private OrderService orderService;
    
    private static final String DELAY_QUEUE_NAME = "order:delay:queue";
    private static final String PROCESSING_SET = "order:delay:processing";
    
    /**
     * 添加订单到延时队列
     * @param orderId 订单ID
     * @param delayTime 延时时间(分钟)
     */
    public void addToDelayQueue(String orderId, long delayTime) {
        RBlockingQueue<String> blockingQueue = redissonClient
            .getBlockingQueue(DELAY_QUEUE_NAME);
        RDelayedQueue<String> delayedQueue = redissonClient
            .getDelayedQueue(blockingQueue);
        
        // 添加到延时队列
        delayedQueue.offer(orderId, delayTime, TimeUnit.MINUTES);
        
        log.info("订单 {} 已添加到延时队列,将在 {} 分钟后过期", orderId, delayTime);
    }
    
    /**
     * 启动延时队列消费者
     */
    @PostConstruct
    public void startDelayQueueConsumer() {
        new Thread(this::consumeDelayQueue, "delay-queue-consumer").start();
    }
    
    private void consumeDelayQueue() {
        RBlockingQueue<String> blockingQueue = redissonClient
            .getBlockingQueue(DELAY_QUEUE_NAME);
        
        while (true) {
            try {
                // 从队列中取出过期的订单
                String orderId = blockingQueue.take();
                
                // 处理订单过期
                processExpiredOrder(orderId);
                
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("延时队列消费线程被中断", e);
                break;
            } catch (Exception e) {
                log.error("处理延时队列消息异常", e);
            }
        }
    }
    
    /**
     * 处理过期订单
     */
    private void processExpiredOrder(String orderId) {
        RSet<String> processingSet = redissonClient.getSet(PROCESSING_SET);
        
        // 使用set防止重复处理
        if (processingSet.add(orderId)) {
            try {
                boolean result = orderService.cancelExpiredOrder(orderId);
                if (result) {
                    log.info("延时队列:订单 {} 已过期取消", orderId);
                } else {
                    log.info("延时队列:订单 {} 无需处理", orderId);
                }
            } finally {
                processingSet.remove(orderId);
            }
        }
    }
}

三、ZSet有序集合方案

typescript 复制代码
@Service
@Slf4j
public class OrderExpireZSetService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String ORDER_EXPIRE_ZSET = "order:expire:zset";
    private static final String ORDER_PROCESSING_SET = "order:processing:set";
    
    /**
     * 添加订单到过期集合
     * @param orderId 订单ID
     * @param expireTime 过期时间戳
     */
    public void addOrderToExpireSet(String orderId, long expireTime) {
        redisTemplate.opsForZSet().add(ORDER_EXPIRE_ZSET, orderId, expireTime);
        log.info("订单 {} 已添加到过期集合,过期时间: {}", orderId, 
                 new Date(expireTime));
    }
    
    /**
     * 批量扫描过期订单
     */
    public void scanExpiredOrders() {
        long now = System.currentTimeMillis();
        
        // 获取已过期的订单
        Set<String> expiredOrders = redisTemplate.opsForZSet()
            .rangeByScore(ORDER_EXPIRE_ZSET, 0, now);
        
        if (expiredOrders != null && !expiredOrders.isEmpty()) {
            for (String orderId : expiredOrders) {
                processExpiredOrder(orderId);
            }
            
            // 移除已处理的订单
            redisTemplate.opsForZSet().removeRangeByScore(
                ORDER_EXPIRE_ZSET, 0, now);
        }
    }
    
    /**
     * 定时扫描任务
     */
    @Scheduled(fixedDelay = 30000) // 每30秒执行一次
    public void scheduledScan() {
        log.debug("开始扫描过期订单");
        scanExpiredOrders();
    }
    
    private void processExpiredOrder(String orderId) {
        // 使用setnx防止重复处理
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(ORDER_PROCESSING_SET + ":" + orderId, "1", 5, TimeUnit.MINUTES);
        
        if (Boolean.TRUE.equals(success)) {
            try {
                // 处理订单过期逻辑
                handleOrderExpired(orderId);
            } finally {
                // 清理处理标记
                redisTemplate.delete(ORDER_PROCESSING_SET + ":" + orderId);
            }
        }
    }
    
    private void handleOrderExpired(String orderId) {
        // TODO: 实现订单过期处理逻辑
        log.info("处理过期订单: {}", orderId);
    }
}

四、完整订单服务实现

1. 订单状态枚举

arduino 复制代码
public enum OrderStatus {
    
    PENDING_PAYMENT(0, "待支付"),
    PAID(1, "已支付"),
    COMPLETED(2, "已完成"),
    CANCELLED(3, "已取消"),
    EXPIRED(4, "已过期");
    
    private final int code;
    private final String description;
    
    OrderStatus(int code, String description) {
        this.code = code;
        this.description = description;
    }
    
    // getter方法省略
}

2. 订单实体

less 复制代码
@Data
@Entity
@Table(name = "t_order")
public class Order {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true)
    private String orderNo;
    
    private Long userId;
    private BigDecimal amount;
    
    @Enumerated(EnumType.ORDINAL)
    private OrderStatus status = OrderStatus.PENDING_PAYMENT;
    
    @Column(name = "expire_time")
    private LocalDateTime expireTime;
    
    @Column(name = "create_time")
    private LocalDateTime createTime = LocalDateTime.now();
    
    @Column(name = "update_time")
    private LocalDateTime updateTime = LocalDateTime.now();
    
    /**
     * 判断订单是否已过期
     */
    public boolean isExpired() {
        return LocalDateTime.now().isAfter(expireTime) 
            && status == OrderStatus.PENDING_PAYMENT;
    }
}

3. 订单服务实现

scss 复制代码
@Service
@Slf4j
@Transactional
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private OrderDelayQueueService delayQueueService;
    
    @Autowired
    private OrderExpireZSetService zSetService;
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    private static final String ORDER_LOCK_PREFIX = "order:lock:";
    private static final String ORDER_EXPIRE_KEY_PREFIX = "order:expire:";
    private static final int ORDER_EXPIRE_MINUTES = 30; // 30分钟未支付过期
    
    /**
     * 创建订单
     */
    public Order createOrder(Long userId, BigDecimal amount) {
        Order order = new Order();
        order.setOrderNo(generateOrderNo());
        order.setUserId(userId);
        order.setAmount(amount);
        order.setStatus(OrderStatus.PENDING_PAYMENT);
        order.setExpireTime(LocalDateTime.now().plusMinutes(ORDER_EXPIRE_MINUTES));
        
        order = orderRepository.save(order);
        
        // 设置Redis过期
        setOrderExpire(order.getOrderNo());
        
        // 添加到延时队列
        delayQueueService.addToDelayQueue(order.getOrderNo(), ORDER_EXPIRE_MINUTES);
        
        // 添加到ZSet
        zSetService.addOrderToExpireSet(
            order.getOrderNo(), 
            order.getExpireTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
        );
        
        log.info("创建订单成功: {}, 过期时间: {}", order.getOrderNo(), order.getExpireTime());
        return order;
    }
    
    /**
     * 设置Redis键过期
     */
    private void setOrderExpire(String orderNo) {
        String key = ORDER_EXPIRE_KEY_PREFIX + orderNo;
        String value = String.valueOf(System.currentTimeMillis());
        
        // 设置30分钟后过期
        redisTemplate.opsForValue().set(
            key, 
            value, 
            ORDER_EXPIRE_MINUTES, 
            TimeUnit.MINUTES
        );
        
        // 同时存储订单信息,用于过期时处理
        Map<String, String> orderInfo = new HashMap<>();
        orderInfo.put("orderNo", orderNo);
        orderInfo.put("userId", "1"); // 实际从订单获取
        orderInfo.put("amount", "100.00");
        
        redisTemplate.opsForHash().putAll(ORDER_KEY_PREFIX + orderNo, orderInfo);
    }
    
    /**
     * 支付成功处理
     */
    public boolean processPayment(String orderNo) {
        // 获取分布式锁
        String lockKey = ORDER_LOCK_PREFIX + orderNo;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        
        if (Boolean.FALSE.equals(locked)) {
            throw new RuntimeException("订单处理中,请稍后");
        }
        
        try {
            Order order = orderRepository.findByOrderNo(orderNo)
                .orElseThrow(() -> new RuntimeException("订单不存在"));
            
            // 检查订单状态
            if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
                throw new RuntimeException("订单状态异常: " + order.getStatus());
            }
            
            // 检查是否过期
            if (order.isExpired()) {
                order.setStatus(OrderStatus.EXPIRED);
                orderRepository.save(order);
                throw new RuntimeException("订单已过期");
            }
            
            // 更新订单状态
            order.setStatus(OrderStatus.PAID);
            order.setUpdateTime(LocalDateTime.now());
            orderRepository.save(order);
            
            // 移除过期设置
            removeOrderExpire(orderNo);
            
            // 发布支付成功事件
            eventPublisher.publishEvent(new OrderPaidEvent(this, order));
            
            log.info("订单支付成功: {}", orderNo);
            return true;
            
        } finally {
            // 释放锁
            redisTemplate.delete(lockKey);
        }
    }
    
    /**
     * 处理过期订单
     */
    public boolean cancelExpiredOrder(String orderNo) {
        String lockKey = ORDER_LOCK_PREFIX + orderNo;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        
        if (Boolean.FALSE.equals(locked)) {
            return false;
        }
        
        try {
            Order order = orderRepository.findByOrderNo(orderNo)
                .orElseThrow(() -> new RuntimeException("订单不存在"));
            
            // 双重检查:订单是否仍为待支付状态
            if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
                log.info("订单 {} 状态已变更为 {},跳过取消", orderNo, order.getStatus());
                return false;
            }
            
            // 检查是否真的过期
            if (!order.isExpired()) {
                log.info("订单 {} 未过期,跳过取消", orderNo);
                return false;
            }
            
            // 更新订单状态
            order.setStatus(OrderStatus.EXPIRED);
            order.setUpdateTime(LocalDateTime.now());
            orderRepository.save(order);
            
            // 释放库存等业务逻辑
            releaseStock(order);
            
            // 发送通知
            sendExpireNotification(order);
            
            log.info("订单 {} 已过期取消", orderNo);
            return true;
            
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
    
    /**
     * 移除订单过期设置
     */
    private void removeOrderExpire(String orderNo) {
        // 删除过期key
        redisTemplate.delete(ORDER_EXPIRE_KEY_PREFIX + orderNo);
        
        // 从延时队列移除
        // 注意:Redisson延时队列不支持直接移除,需要其他方式
        
        // 从ZSet移除
        redisTemplate.opsForZSet().remove(ORDER_EXPIRE_ZSET, orderNo);
        
        // 删除订单缓存
        redisTemplate.delete(ORDER_KEY_PREFIX + orderNo);
    }
    
    /**
     * 生成订单号
     */
    private String generateOrderNo() {
        // 时间戳 + 随机数
        return "ORD" + 
               System.currentTimeMillis() + 
               String.format("%06d", ThreadLocalRandom.current().nextInt(1000000));
    }
    
    private void releaseStock(Order order) {
        // 释放库存逻辑
        log.info("释放订单 {} 的库存", order.getOrderNo());
    }
    
    private void sendExpireNotification(Order order) {
        // 发送通知逻辑
        log.info("发送订单 {} 过期通知", order.getOrderNo());
    }
}

4. 订单支付事件

scala 复制代码
public class OrderPaidEvent extends ApplicationEvent {
    
    private final Order order;
    
    public OrderPaidEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }
    
    public Order getOrder() {
        return order;
    }
}

五、多级过期策略(增强版)

typescript 复制代码
@Component
@Slf4j
public class OrderExpireManager {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String ORDER_EXPIRE_ZSET = "order:expire:zset";
    private static final String ORDER_EXPIRE_DELAY_QUEUE = "order:expire:delay:queue";
    
    /**
     * 三级过期检测策略
     */
    public void startExpireMonitor() {
        // 1. Redis键过期事件(实时)
        // 2. 定时任务扫描(兜底)
        // 3. 延时队列(精确控制)
        
        new Thread(this::monitorDelayQueue, "order-expire-monitor").start();
        new Thread(this::scheduledScan, "order-expire-scanner").start();
    }
    
    /**
     * 监控延时队列
     */
    private void monitorDelayQueue() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 从延时队列获取订单
                String orderId = redisTemplate.opsForList()
                    .rightPop(ORDER_EXPIRE_DELAY_QUEUE, 1, TimeUnit.SECONDS);
                
                if (orderId != null) {
                    processExpiredOrder(orderId);
                }
            } catch (Exception e) {
                log.error("监控延时队列异常", e);
            }
        }
    }
    
    /**
     * 定时扫描
     */
    private void scheduledScan() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                scanExpiredOrders();
                Thread.sleep(30000); // 30秒扫描一次
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                log.error("定时扫描异常", e);
            }
        }
    }
    
    /**
     * 扫描过期订单
     */
    private void scanExpiredOrders() {
        long now = System.currentTimeMillis();
        Set<String> expiredOrders = redisTemplate.opsForZSet()
            .rangeByScore(ORDER_EXPIRE_ZSET, 0, now);
        
        if (expiredOrders != null) {
            for (String orderId : expiredOrders) {
                processExpiredOrder(orderId);
            }
            
            // 移除已处理的订单
            redisTemplate.opsForZSet().removeRangeByScore(
                ORDER_EXPIRE_ZSET, 0, now);
        }
    }
    
    /**
     * 处理过期订单
     */
    private void processExpiredOrder(String orderId) {
        // 防重处理
        String lockKey = "order:expire:process:" + orderId;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 5, TimeUnit.MINUTES);
        
        if (Boolean.TRUE.equals(locked)) {
            try {
                boolean result = orderService.cancelExpiredOrder(orderId);
                if (result) {
                    log.info("成功处理过期订单: {}", orderId);
                }
            } finally {
                redisTemplate.delete(lockKey);
            }
        }
    }
}

六、配置类

typescript 复制代码
@Configuration
@EnableScheduling
public class OrderExpireConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // 设置key和value的序列化方式
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        template.afterPropertiesSet();
        return template;
    }
    
    @Bean
    public OrderExpireManager orderExpireManager() {
        return new OrderExpireManager();
    }
    
    @PostConstruct
    public void init() {
        // 启动过期监控
        orderExpireManager().startExpireMonitor();
    }
}

七、API接口

less 复制代码
@RestController
@RequestMapping("/api/orders")
@Slf4j
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @PostMapping("/create")
    public ApiResponse<Order> createOrder(@RequestBody CreateOrderRequest request) {
        Order order = orderService.createOrder(
            request.getUserId(), 
            request.getAmount()
        );
        return ApiResponse.success(order);
    }
    
    @PostMapping("/{orderNo}/pay")
    public ApiResponse<Void> payOrder(@PathVariable String orderNo) {
        boolean success = orderService.processPayment(orderNo);
        if (success) {
            return ApiResponse.success("支付成功");
        } else {
            return ApiResponse.error("支付失败");
        }
    }
    
    @GetMapping("/{orderNo}/status")
    public ApiResponse<OrderStatus> getOrderStatus(@PathVariable String orderNo) {
        // 从Redis或数据库获取订单状态
        return ApiResponse.success(OrderStatus.PENDING_PAYMENT);
    }
}

八、测试类

ini 复制代码
@SpringBootTest
@Slf4j
class OrderExpireTest {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Test
    void testOrderExpire() throws InterruptedException {
        // 创建订单
        Order order = orderService.createOrder(1L, new BigDecimal("100.00"));
        
        // 验证Redis中已设置过期
        String expireKey = "order:expire:" + order.getOrderNo();
        String value = redisTemplate.opsForValue().get(expireKey);
        assertNotNull(value);
        
        // 验证订单状态
        assertEquals(OrderStatus.PENDING_PAYMENT, order.getStatus());
        
        // 等待订单过期
        Thread.sleep(2000); // 实际应该等30分钟
        
        // 测试支付
        boolean paid = orderService.processPayment(order.getOrderNo());
        assertTrue(paid);
    }
    
    @Test
    void testConcurrentPay() throws InterruptedException {
        Order order = orderService.createOrder(2L, new BigDecimal("200.00"));
        
        ExecutorService executor = Executors.newFixedThreadPool(5);
        CountDownLatch latch = new CountDownLatch(5);
        
        AtomicInteger successCount = new AtomicInteger(0);
        
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                try {
                    boolean result = orderService.processPayment(order.getOrderNo());
                    if (result) {
                        successCount.incrementAndGet();
                    }
                } catch (Exception e) {
                    log.error("支付异常", e);
                } finally {
                    latch.countDown();
                }
            });
        }
        
        latch.await();
        executor.shutdown();
        
        // 只有一个支付成功
        assertEquals(1, successCount.get());
    }
}

九、监控和告警

typescript 复制代码
@Component
@Slf4j
public class OrderExpireMonitor {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Scheduled(fixedRate = 60000) // 每分钟执行一次
    public void monitorExpireOrders() {
        String expireKeyPattern = "order:expire:*";
        
        // 统计待过期订单数量
        Set<String> keys = redisTemplate.keys(expireKeyPattern);
        long expireCount = keys != null ? keys.size() : 0;
        
        // 统计即将在5分钟内过期的订单
        long soonExpireCount = keys.stream()
            .map(key -> redisTemplate.getExpire(key, TimeUnit.SECONDS))
            .filter(ttl -> ttl != null && ttl > 0 && ttl <= 300)
            .count();
        
        // 记录监控日志
        log.info("订单过期监控 - 待过期订单: {}, 即将过期订单: {}", 
                 expireCount, soonExpireCount);
        
        // 发送告警
        if (soonExpireCount > 100) {
            sendAlert("大量订单即将过期: " + soonExpireCount + " 个");
        }
    }
    
    private void sendAlert(String message) {
        // 发送告警到监控系统
        log.warn("订单过期告警: {}", message);
    }
}

总结建议

推荐方案

  1. 生产环境推荐组合方案

    • 主方案:Redisson延时队列 + Redis键过期回调
    • 兜底方案:定时任务扫描ZSet
    • 防重处理:Redis分布式锁
  2. 方案对比

    • Redis键过期回调:实时性最好,但可靠性依赖Redis配置
    • Redisson延时队列:功能强大,支持分布式,推荐使用
    • ZSet定时扫描:实现简单,但实时性较差
    • 多级策略:最可靠,但实现复杂
  3. 注意事项

    • 一定要配置Redis的notify-keyspace-events Ex
    • 考虑网络分区和Redis故障的情况
    • 实现幂等性处理,防止重复取消
    • 添加监控和告警
    • 考虑持久化,防止重启后数据丢失
  4. 性能优化

    • 使用批量处理过期订单
    • 异步处理过期逻辑
    • 合理设置扫描频率
    • 使用连接池

这种实现可以确保订单过期功能的可靠性和实时性,适合电商等高并发场景。

相关推荐
哈哈哈笑什么2 小时前
在高并发分布式SpringCloud系统中,什么时候时候并行查询,提高查询接口效率,从10s到100ms
java·分布式·后端
IMPYLH2 小时前
Lua 的 warn 函数
java·开发语言·笔记·junit·lua
Java水解2 小时前
Django实现接口token检测的实现方案
后端·django
南雨北斗2 小时前
kotlin密封类的主要用途
后端
泉城老铁2 小时前
如何用Spring Boot实现分布式锁?
java·redis·后端
飞Link2 小时前
【Django】Django 调用外部 Python 程序的完整指南
后端·python·django·sqlite
周杰伦_Jay2 小时前
【Java集合与线程池深度解析】底层原理+实战选型+避坑指南(附代码)
java·开发语言·python
老王头的笔记2 小时前
Spring支持的消费器模式,支持在当前事务提交、或回滚的前、后执行业务操作
java·windows·spring
代码or搬砖2 小时前
Java中操作Redis
java·开发语言·redis