库存扣减和订单自动失效

Posted 小哥z

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了库存扣减和订单自动失效相关的知识,希望对你有一定的参考价值。

最近因为身体原因没怎么学习,深深的体会到身体才是最重要的。以后一定加强锻炼。

切入正题,最近项目中需要实现在线挂号功能,初步设计把排班生成的号源看做库存,挂的号看做一个个的订单,生成了订单自动锁号,十分钟不支付自动取消订单,退回号源。

排班那一套就不做详细说明了。

库存扣减和锁

初步设想有几种方案:

1、代码同步, 例如使用 synchronized,lock 等同步方法,看着貌似挺合理的。

但是synchronized 作用范围是单个jvm实例, 如果做了集群,分布式等,就没用了。

而且synchronized是作用在对象实例上的,如果不是单例,则多个实例间不会同步(这个一般用spring管理bean,默认就是单例)

所以这儿就不考虑使用这种方法。

2、不查询,直接更新

    rmcDoctorArrange=getEntity(RmcDoctorArrange.class, doctorArrangeId);
        String sql=" update Rmc_Doctor_Arrange t set t.available_Num=(available_Num-1) where "
                + " id=? and  t.available_Num>‘0‘  ";
        System.out.println(sql);
        result=executeSql(sql,new Object[]{doctorArrangeId});
        System.out.println(result);
        if (result<= 0) {//号源不足
            opFlag="0";
            return opFlag;
            //            }
        }

        // 记录日志...
        // 其他业务...

3、使用数据库锁,select xx for update(悲观锁)

        String sql= "select count(id) from Rmc_Doctor_Arrange where doctor_name = ? for update";
        result=executeSql(sql,new Object[]{doctorArrangeId});
                if (result > 0) {
                    int count = update table set available = (available - 1) where doctor_name = doctorName ;
                } else {
                    return "库存不足";
                }
                //其他业务

在查询时,避免其他用户以该表进行插入,修改或删除等操作,造成表的不一致性,select ** from t for update 会等待行锁释放之后,返回查询结果。

缺点:统一入口:所有库存操作都需要统一使用 select for update ,这样才会阻塞, 如果另外一个方法还是普通的select, 是不会被阻塞的。

加锁顺序:如果有多个锁,那么加锁顺序要一致,否则会出现死锁.

 

4、使用CAS, update table set surplus = aa where id = xx and version = y

public void buy(String productName, Integer buyQuantity) {
    // 其他校验...
    Product product = getByDB(productName);
    int 影响行数 = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus = 查询的剩余数量 ;
    while (result == 0) {
        product = getByDB(productName);
        if (查询的剩余数量 > buyQuantity) {
            影响行数 = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus = 查询的剩余数量 ;
        } else {
            return "库存不足";
        }
    }
    
    // 记录日志...
    // 其他业务...
}

这儿会造成一个经典的ABA问题,所以我们可以给数据库加上版本号。

update t set surplus = 90 ,version = version+1 where id = x and version = oldVersion ;

项目最终采用了这种方式

5、使用redis等一些分布式锁实现。

订单自动失效

订单失效也经历了几种解决办法。

1、后台线程扫描,业务量大了之后服务器肯定会吃不住的。不考虑

2、考虑过放入redis中,失效时间24小时,如果订单生成,写入库中,删除red is中的订单。(最后考虑到这个项目中redis用处不太大,不想集成进来)

3、定时任务,每分钟定时扫一次,先不说时间上有偏差(用户并不会察觉或者并不怎么关心是否刚好是十分钟取消订单),光是每分钟扫做一下订单表的扫描就觉得可怕。

4、delayedQueue延时队列。

delayedQueue本质是由PriorityQueue和BlockingQueue实现的阻塞优先级队列

我们可以将生成的订单按顺序放入delayedQueue中,设置到期时间,其中的对象只能在到期时才能从队列中取走,它又符合队列的先进先出的设计原理,简直完美的适合订单超时取消。

但是最终也放弃了这种方案,因为它不支持分布式、不支持分布式、不支持分布式。重要的事说三遍。

5、RabbitMQ和ActiveMq

虽然网上都说RabbitMQ性能要优越很多(没有具体测试),但是最终还是选择了ActiveMq,因为考虑到ActiveMq是用java写的,可能会更适合java项目。

ActiveMq

ActiveMQ官网下载地址:http://activemq.apache.org/download.html

可以去选择适合的版本下载,安装都很简单。

1、集成到spring中。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:amq="http://activemq.apache.org/schema/core"
       xmlns:jms="http://www.springframework.org/schema/jms"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.1.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
        http://www.springframework.org/schema/jms
        http://www.springframework.org/schema/jms/spring-jms-4.1.xsd
        http://activemq.apache.org/schema/core
        http://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd">



<!--     <context:property-placeholder location="classpath:sysConfig.properties" ignore-unresolvable="true" /> -->
    
    <amq:connectionFactory id="amqConnectionFactory"
                           brokerURL="tcp://localhost:61616/admin"
                           userName="admin"
                           password="admin" />

    <!-- 配置JMS连接工长 -->
    <bean id="connectionFactory"
          class="org.springframework.jms.connection.CachingConnectionFactory">
        <constructor-arg ref="amqConnectionFactory" />
        <property name="sessionCacheSize" value="100" />
    </bean>

    <!-- 定义消息队列(Queue) -->
    <bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
        <!-- 设置消息队列的名字 -->
         <constructor-arg value="rmcOrder.queue"/>  
    </bean>

    <!-- 配置JMS模板(Queue),Spring提供的JMS工具类,它发送、接收消息。 -->
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="defaultDestination" ref="destination" />
        <property name="receiveTimeout" value="10000" />
        <!-- true是topic,false是queue,默认是false,此处显示写出false -->
        <property name="pubSubDomain" value="false" />
    </bean>


    <!-- 配置消息队列监听者(Queue) -->
    <bean id="queueMessageListener" class="com.cdxt.bll.online.listener.QueueMessageListener" />

    <!-- 显示注入消息监听容器(Queue),配置连接工厂,监听的目标是demoQueueDestination,监听器是上面定义的监听器      -->
    <bean id="queueListenerContainer"
          class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="destination" ref="destination" />
        <property name="messageListener" ref="queueMessageListener" />
    </bean>

</beans>

生成订单后将id返回放入队列中。

        if(StringUtil.isNotEmpty(id)){
            sendTxtMessage(destination,id);
        }
    public void sendTxtMessage(Destination destination, final String message) {  
        if (null == destination) {  
            destination = jmsTemplate.getDefaultDestination();  
        }
        String delay=null;
        delay=ResourceUtil.getConfigByName("queneDelay");
        if(StringUtil.isNotEmpty(delay)){

            Long queneDelay=Long.parseLong(delay)*60*1000;
            jmsTemplate.convertAndSend((Object)message, new ScheduleMessagePostProcessor(queneDelay));//

        }
        //        jmsTemplate.send(destination, new MessageCreator() {  
        //            public Message createMessage(Session session) throws JMSException {  
        //                session.setMessageListener(arg0);
        //                return session.createTextMessage(message);  
        //            }  
        //        });  
        System.out.println("springJMS send text message...");  
    }  

监听器监听

public class QueueMessageListener implements MessageListener {

    @Resource
    private RmcRegisterService rmcRegisterService;

    public void onMessage(Message message) {
        TextMessage tm = (TextMessage) message;
        try {
            System.out.println("QueueMessageListener监听到了文本消息:\t"
                    + tm.getText());
            String id=tm.getText();
            RmcOrder order=rmcRegisterService.getEntity(RmcOrder.class, id);
            System.out.println("order:"+JsonUtil.bean2json(order));
            System.out.println(new Date());
            if("01".equals(order.getRmcStatus())){
//取消订单 String sql
=" update Rmc_order set rmc_status=? where id=? "; Integer cont=rmcRegisterService.executeSql(sql,new Object[]{"02",id}); System.out.println("count:"+cont); } } catch (JMSException e) { e.printStackTrace(); } } }

生成订单后,可以在查看队列的详细信息http://127.0.0.1:8161/admin/。

后期优化方向:成功付款或者手动取消了的订单可否移出队列或者简化这些类型订单的处理?

以上是关于库存扣减和订单自动失效的主要内容,如果未能解决你的问题,请参考以下文章

解决库存扣减及订单创建时防止并发死锁的问题

精品seata综合示例:订单-库存-扣款

16 基于MQ实现秒杀订单系统的异步化架构以及精准扣减库存的技术方案

微服务的自动化集成测试实战

分布式一致性的想法

订单减库存设计