013-3 订单支付超时自动取消订单(rocketmq mybatis)


MyBatis需要字段和数据库列对应。假设数据库表名是orders,字段名与类属性一致,或者通过配置map-underscore-to-camel-case自动转换。如果有不同,可能需要使用@Column注解,但MyBatis本身不支持JPA注解,除非使用MyBatis的注解,比如@Table、@Column等,或者在XML中配置resultMap。
不过,MyBatis的注解方式可能不够灵活,所以可能更倾向于使用XML配置resultMap。或者,如果数据库字段和实体类属性名符合下划线转驼峰规则,可以不用显式配置。

原来的JPA中,@Version注解会自动处理版本号,但MyBatis需要手动初始化version为0或1。

在update操作时,SQL语句中version = version + 1,同时需要检查版本号是否匹配,但原来的JPA方法中的update语句没有使用version作为条件,这可能有问题。例如,原来的JPA的payOrder方法是:

UPDATE Order o SET o.status = 'PAID', o.version = o.version + 1 WHERE o.orderId = :orderId AND o.status = 'UNPAID'

这里只检查status是否为UNPAID,而没有检查version。这可能是因为在高并发下,乐观锁可能无法完全避免问题,但原代码可能依赖数据库的条件更新来保证原子性。在MyBatis中,同样需要保持相同的条件。

因此,原来的payOrder和cancelUnpaidOrder的SQL语句中的条件是正确的,不需要额外的version检查,除非需要实现乐观锁。但原JPA代码中的@Version可能用于乐观锁,但MyBatis中需要手动处理。但原代码中的update语句并没有使用version作为条件,所以可能乐观锁并未实际应用。这可能是一个问题,需要确认原代码的设计意图。例如,@Version在JPA中通常用于乐观锁,每次更新时检查version的值,但原代码的update语句并没有包含version的条件,所以可能实际上没有使用乐观锁。这可能是一个错误,或者原代码的设计中没有正确使用乐观锁。

在原来的Order实体中有@Version注解,但Repository中的update语句并没有在where子句中使用version字段,这会导致@Version失效,因为JPA在执行update时会自动添加version的条件。但用户提供的Repository中的@Query手动编写了update语句,而没有包含version的条件,这可能导致乐观锁失效。这可能是一个问题,用户可能希望使用version来防止并发更新,但当前的实现并没有做到这一点。

不过,在用户的问题中,这部分可能需要保留原来的逻辑,即不使用version作为条件,因此转换为MyBatis时,同样不需要在update语句中包含version的条件。但需要注意,这样可能导致并发问题,但可能用户的设计中通过其他方式(如分布式锁)来处理。

Redisson 分布式锁的作用范围
正确理解:
Redisson 的分布式锁(RLock)确实能够跨所有 JVM 实例,包括同一 JVM 内的多个线程。其核心机制是通过 Redis 的原子操作实现全局互斥。
跨 JVM:不同服务实例的线程会竞争同一把锁,只有一个线程能成功获取锁。
同一 JVM 内:Redisson 的锁在单个 JVM 内也会通过本地锁(如 ReentrantLock)实现线程互斥。
即使多个线程在同一个 JVM 内,也会遵循“同一时刻只有一个线程持有锁”的规则。

为什么仍需数据库乐观锁?
即使有分布式锁,仍需数据库乐观锁的 双重保障,原因如下:
(1) 锁超时风险
锁自动释放:Redisson 默认的看门狗机制(锁续期)会保证业务执行期间锁不超时,但极端情况(如 Full GC 导致线程暂停)可能导致锁过期后被其他线程获取。
兜底机制:数据库乐观锁(版本检查)可以防止锁失效后的并发修改。

(2) 数据库事务隔离性
读已提交(Read Committed):事务中读取的数据可能已被其他事务修改,导致后续操作基于过期数据。
幻读(Phantom Read):即使有锁,其他事务可能插入新数据影响当前事务逻辑。

(3) 缓存与数据库不一致
缓存中的订单状态可能未及时更新,导致双重检查(orderCache.getIfPresent)失效。

实际场景分析
假设分布式锁因网络波动短暂失效,导致两个线程同时进入临界区:
线程 A(JVM-A)获取锁,更新数据库成功(version=2)。
线程 B(JVM-B)在锁超时后获取锁,但缓存未及时失效,仍读取到旧状态(status=UNPAID)。
无版本检查:线程 B 的 UPDATE … WHERE status=‘UNPAID’ 可能意外覆盖线程 A 的更新(如订单已支付但状态被错误修改)。
有版本检查:线程 B 的 UPDATE … WHERE version=1 会因版本不匹配失败,避免错误更新。

1. 实体类 Order.java

public class Order {
    private String orderId;
    private OrderStatus status;
    private Long createTime;
    private Integer version;
    
    // Getters and Setters
}

public enum OrderStatus {
    UNPAID, PAID, CANCELLED
}

2. MyBatis Mapper 接口 OrderMapper.java

@Mapper
public interface OrderMapper {
    @Insert("INSERT INTO orders (order_id, status, create_time, version) " +
            "VALUES (#{orderId}, #{status}, #{createTime}, #{version})")
    int insert(Order order);

    @Select("SELECT * FROM orders WHERE order_id = #{orderId}")
    Order selectById(String orderId);

    @Update("UPDATE orders SET status = 'PAID', version = version + 1 " +
            "WHERE order_id = #{orderId} " +
            "AND status = 'UNPAID' " +
            "AND version = #{currentVersion}")
    int payWithVersion(@Param("orderId") String orderId, 
                      @Param("currentVersion") int currentVersion);

    @Update("UPDATE orders SET status = 'CANCELLED', version = version + 1 " +
            "WHERE order_id = #{orderId} " +
            "AND status = 'UNPAID' " +
            "AND version = #{currentVersion}")
    int cancelWithVersion(@Param("orderId") String orderId,
                         @Param("currentVersion") int currentVersion);

    @Select("SELECT * FROM orders WHERE create_time <= #{threshold} AND status = 'UNPAID'")
    List<Order> selectExpiredUnpaid(long threshold);
}

3. 服务层 OrderService.java

@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderMapper orderMapper;
    private final Cache<String, OrderStatus> orderCache;
    private final RedissonClient redisson;
    private final OrderTimeoutProducer timeoutProducer;

    private static final int LOCK_WAIT_TIME = 1; // 秒
    private static final int LOCK_LEASE_TIME = 30; // 秒

    @Transactional
    public String createOrder() {
        String orderId = UUID.randomUUID().toString();
        Order order = new Order();
        order.setOrderId(orderId);
        order.setStatus(OrderStatus.UNPAID);
        order.setCreateTime(System.currentTimeMillis());
        order.setVersion(0);
        orderMapper.insert(order);
        
        orderCache.put(orderId, OrderStatus.UNPAID);
        timeoutProducer.sendTimeoutMessage(orderId);
        return orderId;
    }

    public boolean safePay(String orderId) {
        RLock lock = redisson.getLock("order:pay:" + orderId);
        try {
            if (lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS)) {
                // 缓存快速检查
                OrderStatus cachedStatus = orderCache.getIfPresent(orderId);
                if (cachedStatus != null && cachedStatus != OrderStatus.UNPAID) {
                    return false;
                }

                // 查询数据库最新状态
                Order dbOrder = orderMapper.selectById(orderId);
                if (dbOrder == null || dbOrder.getStatus() != OrderStatus.UNPAID) {
                    orderCache.put(orderId, dbOrder != null ? dbOrder.getStatus() : null);
                    return false;
                }

                // 带版本号的更新
                int affected = orderMapper.payWithVersion(orderId, dbOrder.getVersion());
                if (affected > 0) {
                    orderCache.put(orderId, OrderStatus.PAID);
                    return true;
                }

                // 处理版本冲突
                throw new OptimisticLockException("订单支付冲突,版本号已变更");
            }
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    @Transactional
    public boolean safeCancel(String orderId) {
        // 缓存快速失败
        if (orderCache.getIfPresent(orderId) != OrderStatus.UNPAID) {
            return false;
        }

        // 查询数据库最新状态
        Order dbOrder = orderMapper.selectById(orderId);
        if (dbOrder == null || dbOrder.getStatus() != OrderStatus.UNPAID) {
            orderCache.put(orderId, dbOrder != null ? dbOrder.getStatus() : null);
            return false;
        }

        // 带版本号的更新
        int affected = orderMapper.cancelWithVersion(orderId, dbOrder.getVersion());
        if (affected > 0) {
            orderCache.put(orderId, OrderStatus.CANCELLED);
            return true;
        }
        return false;
    }
}

4. RocketMQ 消息处理

生产者 OrderTimeoutProducer.java

@Component
@RequiredArgsConstructor
public class OrderTimeoutProducer {
    private final RocketMQTemplate rocketMQTemplate;

    public void sendTimeoutMessage(String orderId) {
        Message<String> message = MessageBuilder.withPayload(orderId)
                .setHeader(RocketMQHeaders.KEYS, orderId)
                .build();
        // 使用延迟等级4(对应30分钟)
        rocketMQTemplate.syncSend("ORDER_TIMEOUT_TOPIC", message, 3000, 4);
    }
}

消费者 OrderTimeoutConsumer.java

@Service
@RocketMQMessageListener(
    topic = "ORDER_TIMEOUT_TOPIC",
    consumerGroup = "order-timeout-group",
    messageModel = MessageModel.CLUSTERING
)
@Slf4j
@RequiredArgsConstructor
public class OrderTimeoutConsumer implements RocketMQListener<MessageExt> {
    private final OrderService orderService;
    private final Cache<String, Boolean> processedMsgCache = 
        Caffeine.newBuilder()
            .expireAfterWrite(1, TimeUnit.HOURS)
            .maximumSize(10000)
            .build();

    @Override
    public void onMessage(MessageExt message) {
        String msgId = message.getMsgId();
        if (processedMsgCache.getIfPresent(msgId) != null) {
            return;
        }

        String orderId = new String(message.getBody(), StandardCharsets.UTF_8);
        try {
            boolean success = orderService.safeCancel(orderId);
            if (success) {
                processedMsgCache.put(msgId, true);
                log.info("订单超时取消成功: {}", orderId);
            }
        } catch (Exception e) {
            log.error("订单取消处理失败: {}", orderId, e);
            throw new RuntimeException("处理超时订单失败", e);
        }
    }
}

5. 缓存配置 CacheConfig.java

@Configuration
public class CacheConfig {
    @Bean
    public Cache<String, OrderStatus> orderStatusCache() {
        return Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();
    }
}

6. 对账服务 ReconciliationTask.java

@Component
@RequiredArgsConstructor
@Slf4j
public class ReconciliationTask {
    private final OrderMapper orderMapper;
    private final OrderTimeoutProducer timeoutProducer;

    @Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
    public void checkUnpaidOrders() {
        long threshold = LocalDateTime.now()
            .minusMinutes(30)
            .atZone(ZoneId.systemDefault())
            .toInstant()
            .toEpochMilli();

        List<Order> expiredOrders = orderMapper.selectExpiredUnpaid(threshold);
        expiredOrders.forEach(order -> {
            log.info("发现未处理超时订单: {}", order.getOrderId());
            timeoutProducer.sendTimeoutMessage(order.getOrderId());
        });
    }
}

7. 控制器 OrderController.java

@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;

    @PostMapping
    public ResponseEntity<String> createOrder() {
        String orderId = orderService.createOrder();
        return ResponseEntity.ok(orderId);
    }

    @PostMapping("/{orderId}/pay")
    public ResponseEntity<?> payOrder(@PathVariable String orderId) {
        return orderService.safePay(orderId) ?
            ResponseEntity.ok().build() :
            ResponseEntity.status(HttpStatus.CONFLICT).body("支付失败");
    }
}

关键改进说明
双重一致性保障:
分布式锁(Redisson)控制跨进程/线程并发
数据库乐观锁(版本号)兜底数据最终一致性
缓存双重检查快速过滤无效请求

防御性编程:

// 在锁内重新查询数据库最新状态
Order dbOrder = orderMapper.selectById(orderId);
if (dbOrder == null || dbOrder.getStatus() != OrderStatus.UNPAID) {
    orderCache.put(orderId, dbOrder != null ? dbOrder.getStatus() : null);
    return false;
}

异常处理优化:

// 显式抛出乐观锁异常
throw new OptimisticLockException("订单支付冲突,版本号已变更");

消息去重机制:

// 使用Caffeine缓存已处理消息ID
if (processedMsgCache.getIfPresent(msgId) != null) {
    return;
}

锁释放保护:

finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

数据库表结构(MySQL)

CREATE TABLE orders (
    order_id VARCHAR(36) PRIMARY KEY,
    status VARCHAR(20) NOT NULL,
    create_time BIGINT NOT NULL,
    version INT NOT NULL DEFAULT 0
);

CREATE INDEX idx_status ON orders(status);
CREATE INDEX idx_create_time ON orders(create_time);

该实现完整覆盖了以下场景:
订单创建与超时取消
高并发支付场景下的数据一致性
分布式系统间的消息可靠处理
每日对账补偿机制
缓存与数据库的协同工作
通过 Redisson 分布式锁 + 数据库乐观锁 + 缓存三重防护,可有效应对各类并发场景,确保系统强一致性。

配置

application.yml
需要补充数据库连接池的参数,比如HikariCP的配置,以提高数据库访问性能。此外,MyBatis的配置可能需要更详细的设置,比如是否开启缓存、日志级别等。
Redisson的配置可能需要一个单独的redisson.yml文件,用于设置Redis的连接参数、线程池、超时时间等,以确保分布式锁的性能和可靠性。同时,SSL配置对于安全性很重要,特别是生产环境中需要启用数据库的SSL连接,并配置信任证书。
关于RocketMQ,需要确保生产者和消费者的参数合理,比如重试次数、超时时间,以及消息的持久化策略。此外,Caffeine缓存的配置需要根据实际业务需求调整,比如最大容量、过期时间,以及可能的刷新策略,以平衡内存使用和性能。
安全性方面,除了数据库的SSL,还需要考虑敏感信息的加密,比如使用Spring Cloud Config Server或者Vault来管理密码,避免明文存储。此外,API端点可能需要添加安全层,比如Spring Security的整合
日志配置也是重要的一环,合理的日志级别和输出格式有助于监控和故障排查。需要配置Logback或Log4j2,确保日志文件轮转和归档,避免磁盘空间被占满。
还需要考虑应用服务器的配置,比如Tomcat的参数调整,最大线程数、连接超时等,以应对高并发场景。同时,JVM参数的优化也是性能调优的一部分,但可能在启动脚本中配置,而非应用配置文件。
在整合这些配置时,需要确保各部分的参数协调一致,避免资源竞争或配置冲突。例如,数据库连接池的最大连接数需要与Tomcat的线程数匹配,避免连接不足或浪费。同样,Redisson的线程池设置需要与系统资源相匹配,确保不会耗尽内存或CPU。
可能遇到的问题包括配置参数的理解错误,比如HikariCP的max-lifetime和idle-timeout的区别,或者Redisson的netty线程数设置不合理导致性能瓶颈。
此外,测试也是关键的一环。在配置完成后,需要进行压力测试,观察系统的表现,根据结果调整参数,比如调整连接池大小、缓存容量、线程池参数等,以达到最优性能。

1. 应用主配置 application.yml

# 应用基础配置
spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://${DB_HOST:localhost}:3306/order_db?useSSL=true&requireSSL=true&verifyServerCertificate=false
    username: ${DB_USER:root}
    password: ${DB_PASSWORD:root}
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      pool-name: OrderHikariPool
      minimum-idle: 10
      maximum-pool-size: 50
      idle-timeout: 30000
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
  jackson:
    time-zone: Asia/Shanghai
    date-format: yyyy-MM-dd HH:mm:ss

  # MyBatis 配置
mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.domain
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false  # 禁用二级缓存,使用自定义缓存
    default-statement-timeout: 30
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

# RocketMQ 配置
rocketmq:
  name-server: ${ROCKETMQ_NS:localhost:9876}
  producer:
    group: order-producer-group
    send-message-timeout: 3000
    retry-times-when-send-failed: 2
    max-message-size: 4194304 # 4MB
  consumer:
    pull-batch-size: 32

# Caffeine 缓存配置
caffeine:
  order-status:
    spec: maximumSize=10000,expireAfterWrite=10m

# Redisson 配置(指向外部配置文件)
redisson:
  file: classpath:redisson-config.yml

# 安全配置(示例)
security:
  require-ssl: true
  jwt:
    secret: ${JWT_SECRET:defaultStrongSecretKey}
    expiration: 86400

# 日志配置
logging:
  level:
    root: info
    com.example: debug
  file:
    name: logs/order-service.log
  pattern:
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

# 服务监控
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

2. Redisson 配置 redisson-config.yml

singleServerConfig:
  address: "redis://${REDIS_HOST:127.0.0.1}:6379"
  password: ${REDIS_PASSWORD:}
  database: 0
  connectionMinimumIdleSize: 10
  connectionPoolSize: 64
  subscriptionConnectionPoolSize: 50
  dnsMonitoringInterval: 5000

threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: NIO

3. MySQL SSL 配置(可选)

在 src/main/resources 添加 ssl 目录存放:
client-cert.pem
client-key.pem
ca.pem
调整 JDBC URL:

url: jdbc:mysql://${DB_HOST}:3306/order_db?
  useSSL=true&
  requireSSL=true&
  verifyServerCertificate=true&
  clientCertificateKeyStoreUrl=file:ssl/client-cert.pem&
  clientCertificateKeyStorePassword=123456&
  trustCertificateKeyStoreUrl=file:ssl/ca.pem&
  trustCertificateKeyStorePassword=123456

4. 性能调优配置补充

JVM 参数建议(启动脚本):

java -jar order-service.jar \
  -Xms2g -Xmx2g \
  -XX:MaxMetaspaceSize=512m \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:ParallelGCThreads=4 \
  -Djava.security.egd=file:/dev/./urandom

5. 安全加固措施

(1) 敏感信息加密(使用 jasypt)
添加依赖:

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>

配置加密密码:

jasypt:
  encryptor:
    password: ${JASYPT_PASSWORD:defaultEnvPassword}
    algorithm: PBEWITHHMACSHA512ANDAES_256
    iv-generator-classname: org.jasypt.iv.RandomIvGenerator

加密数据库密码:

java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \
  input="real_password" \
  password="master_key" \
  algorithm=PBEWITHHMACSHA512ANDAES_256

替换配置:

password: ENC(加密后的字符串)

6. 配置说明

配置项作用说明
HikariCP 连接池配置优化数据库连接使用,防止连接泄露
RocketMQ 生产者重试机制确保消息可靠投递,应对网络抖动
Redisson 线程池配置平衡Redis连接资源使用与并发处理能力
Caffeine 缓存过期策略防止内存泄漏,平衡缓存命中率与数据一致性
MyBatis 语句超时设置避免慢查询拖垮数据库
JVM G1 GC 参数优化垃圾回收性能,减少服务暂停时间
SSL 双向认证配置保障数据传输安全,防止中间人攻击
监控端点配置提供健康检查、指标收集等运维能力

7. 生产环境部署建议

配置中心:
使用 Nacos/Apollo 管理动态配置,实现热更新

密钥管理:
数据库密码、Redis密码等敏感信息使用Vault等密钥管理服务
禁止在代码仓库中存储明文密码

网络策略:
数据库、Redis、RocketMQ等中间件部署在内网
开启防火墙白名单限制

监控报警:
通过 Prometheus + Grafana 监控以下指标:
接口QPS/RT
缓存命中率
数据库连接池使用率
RocketMQ积压消息数

压测验证:
使用 JMeter 进行以下场景测试:
10,000 TPS 下单支付
30分钟持续负载
网络分区模拟测试

该配置方案实现了:
✅ 高性能:通过连接池优化、缓存策略、合理线程配置
✅ 高安全:SSL传输加密、敏感信息保护、访问控制
✅ 可观测:完善的监控指标和日志记录
✅ 可靠性:消息重试机制、数据库乐观锁兜底

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凡所有相 皆是虚妄。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值