超时订单自动关闭的优雅实现

1.任务场景

12306订单

在这里插入图片描述

3.实现方式

3.1 数据库轮询(30S)

订单表(订单ID,状态,创建时间)

在这里插入图片描述
轮询数据库会带来什么问题?

1、轮询大部分时间其实是在做无用功,我们假设一张订单是45分钟过期,每分钟我们扫描一次,对这张订单来说,要扫描45次以后,才会检查到这张订单过期,这就意味着数据库的资源(连接,IO)被白白浪费了;

2、处理上的不及时,一个待支付的电影票订单我们假设是12:00:35过期,但是上次扫描的时间是12:00:30,那么这个订单实际的过期时间是什么时候?12:01:30,和我本来的过期时间差了55秒钟。放在业务上,会带来什么问题?这张电影票,假设是最后一张,有个人12:00:55来买票,买得到吗?当然买不到了。那么这张电影票很有可能就浪费了。如果缩短扫描的时间间隔,第一只能改善不能解决,第二,又会对数据库造成更大的压力。

3.2 JDK 队列

使用DelayQueue 实现

DelayQueue: 阻塞队列(先进先出)

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满

2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

Delayed接口使对象成为延迟对象,它使存放在DelayQueue类中的对象具有了激活日期。该接口强制实现下列两个方法。

实现代码

/**
 *类说明:存放到延迟队列的元素,对业务数据进行了包装
 */
public class ItemVo<T> implements Delayed{
    
    
	//到期时间,但传入的数值代表过期的时长,传入单位毫秒
    private long activeTime;
    private T data;//业务数据,泛型
    
	public ItemVo(long activeTime, T data) {
    
    
		super();
		this.activeTime = activeTime + System.currentTimeMillis();
		this.data = data;
	}

	public long getActiveTime() {
    
    
		return activeTime;
	}

	public T getData() {
    
    
		return data;
	}
	
	/*
	 * 这个方法返回到激活日期的剩余时间,时间单位由单位参数指定。
	 */
	public long getDelay(TimeUnit unit) {
    
    
        long d = unit.convert(this.activeTime - System.currentTimeMillis(), 
        		unit);
        return d;
	}

	/*
     *Delayed接口继承了Comparable接口,按剩余时间排序,实际计算考虑精度为纳秒数
	 */
	public int compareTo(Delayed o) {
    
    
        long d = (getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS));
        return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
	}



}



public class OrderExp {
    
    

	private Long id;

    private String orderNo;

    private String orderNote;

    private Date insertTime;

    private Long expireDuration;

    private Date expireTime;

    /*0:未支付;1:已支付;-1:已过期,关闭*/
    private Short orderStatus;

    public OrderExp(Long id, String orderNo, String orderNote, Date insertTime, 
    		Long expireDuration, Date expireTime, Short orderStatus) {
    
    
        this.id = id;
        this.orderNo = orderNo;
        this.orderNote = orderNote;
        this.insertTime = insertTime;
        this.expireDuration = expireDuration;
        this.expireTime = expireTime;
        this.orderStatus = orderStatus;
    }

    public OrderExp() {
    
    
        super();
    }

    public Long getId() {
    
    
        return id;
    }

    public void setId(Long id) {
    
    
        this.id = id;
    }

    public String getOrderNo() {
    
    
        return orderNo;
    }

    public void setOrderNo(String orderNo) {
    
    
        this.orderNo = orderNo == null ? null : orderNo.trim();
    }

    public String getOrderNote() {
    
    
        return orderNote;
    }

    public void setOrderNote(String orderNote) {
    
    
        this.orderNote = orderNote == null ? null : orderNote.trim();
    }

    public Date getInsertTime() {
    
    
        return insertTime;
    }

    public void setInsertTime(Date insertTime) {
    
    
        this.insertTime = insertTime;
    }

    public Long getExpireDuration() {
    
    
        return expireDuration;
    }

    public void setExpireDuration(Long expireDuration) {
    
    
        this.expireDuration = expireDuration;
    }

    public Date getExpireTime() {
    
    
        return expireTime;
    }

    public void setExpireTime(Date expireTime) {
    
    
        this.expireTime = expireTime;
    }

    public Short getOrderStatus() {
    
    
        return orderStatus;
    }

    public void setOrderStatus(Short orderStatus) {
    
    
        this.orderStatus = orderStatus;
    }

	@Override
	public String toString() {
    
    
		return "OrderExp [id=" + id + ", orderNo=" + orderNo 
				+ ", orderNote=" + orderNote + ", orderStatus="
				+ orderStatus + "]";
	}
    
    
}

sql语句

DROP TABLE IF EXISTS `order_exp`;
CREATE TABLE `order_exp` (
  `id` bigint(22) NOT NULL AUTO_INCREMENT COMMENT '订单的主键',
  `order_no` varchar(50) NOT NULL COMMENT '订单的编号',
  `order_note` varchar(100) NOT NULL COMMENT '订单的说明',
  `insert_time` datetime NOT NULL COMMENT '插入订单的时间',
  `expire_duration` bigint(22) NOT NULL COMMENT '订单的过期时长,单位秒',
  `expire_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '订单的过期时间',
  `order_status` smallint(6) NOT NULL DEFAULT '0' COMMENT '订单的状态,0:未支付;1:已支付;-1:已过期,关闭',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of order_exp
-- ----------------------------


@Repository
public interface OrderExpDao {
    
    
    int deleteByPrimaryKey(Long id);

    int insert(OrderExp record);

    int insertSelective(OrderExp record);

    OrderExp selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(OrderExp record);

    int updateByPrimaryKey(OrderExp record);
    
    /*插入延迟订单*/
    int insertDelayOrder(@Param("order") OrderExp order,
                         @Param("expire_duration") long expire_duration);
    
    /*将指定id且未支付订单的状态改为已过期*/
    int updateExpireOrder(Long id);
    
    /*将表中所有时间上已过期但未支付订单的状态改为已过期*/
    int updateExpireOrders();
    
    /*找出未支付且未过期的订单 */
    List<OrderExp> selectUnPayOrders();
    
//    /*找出未支付且已过期的订单*/
//    List<OrderExp> selectExpiredOrders();

}

public interface IDelayOrder {
    
    

	/**
	 * 进行延时处理的方法
	 * @param order 要进行延时处理的订单
	 * @param expireTime 延时时长,单位秒
	 */
    public void orderDelay(OrderExp order, long expireTime);

}



@Service
@Qualifier("dq")
public class DqMode implements IDelayOrder {
    
    
	
	private Logger logger = LoggerFactory.getLogger(DqMode.class);
	
	@Autowired
	private DlyOrderProcessor processDelayOrder;
	private Thread takeOrder;
	
	private static DelayQueue<ItemVo<OrderExp>> delayOrder
		= new DelayQueue<ItemVo<OrderExp>>();

    public void orderDelay(OrderExp order, long expireTime) {
    
    
    	ItemVo<OrderExp> itemOrder = new ItemVo<OrderExp>(expireTime*1000,order);
    	delayOrder.put(itemOrder);
    	logger.info("订单[超时时长:"+expireTime+"秒]被推入检查队列,订单详情:"+order);
    }
    
    private class TakeOrder implements Runnable{
    
    
    	
    	private DlyOrderProcessor processDelayOrder;

		public TakeOrder(DlyOrderProcessor processDelayOrder) {
    
    
			super();
			this.processDelayOrder = processDelayOrder;
		}

		public void run() {
    
    
			logger.info("处理到期订单线程已经启动!");
			while(!Thread.currentThread().isInterrupted()) {
    
    
				try {
    
    
					ItemVo<OrderExp> itemOrder = delayOrder.take();
					if (itemOrder!=null) {
    
    
						processDelayOrder.checkDelayOrder(itemOrder.getData());
					}
				} catch (Exception e) {
    
    
					logger.error("The thread :",e);
				}
			}
			logger.info("处理到期订单线程准备关闭......");
		}
    }
    
//    @PostConstruct
//    public void init() {
    
    
//    	takeOrder = new Thread(new TakeOrder(processDelayOrder));
//    	takeOrder.start();
//    }
//
//    @PreDestroy
//    public void close() {
    
    
//    	takeOrder.interrupt();
//    }
}



//类说明:处理延期订单的服务
@Service
public class DlyOrderProcessor {
    
    
	private Logger logger = LoggerFactory.getLogger(DlyOrderProcessor.class);
	
	@Autowired
	private OrderExpDao orderExpDao;
	
	/**检查数据库中指定id的订单的状态,如果为未支付,则修改为已过期*/
	public void checkDelayOrder(OrderExp record) {
    
    
		OrderExp dbOrder = orderExpDao.selectByPrimaryKey(record.getId());
		if(dbOrder.getOrderStatus()==SaveOrder.UNPAY) {
    
    
			logger.info("订单【"+record+"】未支付已过期,需要更改为过期订单!");
			orderExpDao.updateExpireOrder(record.getId());
		}else {
    
    
			logger.info("已支付订单【"+record+"】,无需修改!");
		}
		
	}

}



//类说明:订单相关的服务
@Service
public class SaveOrder {
    
    
	
	private Logger logger = LoggerFactory.getLogger(SaveOrder.class);
	
	public final static short UNPAY = 0;
	public final static short PAYED = 1;
	public final static short EXPIRED = -1;
	
	@Autowired
	private OrderExpDao orderExpDao;
	
	@Autowired
	@Qualifier("mq")
	private IDelayOrder delayOrder;

	/**
	 * 接收前端页面参数,生成订单
	 * @param orderNumber 订单个数
	 */
    public void insertOrders(int orderNumber){
    
    
    	Random r = new Random();
    	OrderExp orderExp ;
    	for(int i=0;i<orderNumber;i++) {
    
    
    		long expireTime = r.nextInt(20)+5;//订单的超时时长,单位秒
            orderExp = new OrderExp();
            String orderNo = "DD00_"+expireTime+"S";
            orderExp.setOrderNo(orderNo);
            orderExp.setOrderNote("享学订单——"+orderNo);
            orderExp.setOrderStatus(UNPAY);
            orderExpDao.insertDelayOrder(orderExp,expireTime);  
            logger.info("保存订单到DB:"+orderNo);
            delayOrder.orderDelay(orderExp, expireTime);
    	}
    }
    
//    @PostConstruct
//    public void initDelayOrder() {
    
    
//    	logger.info("系统启动,扫描表中过期未支付的订单并处理.........");
//
//    	int counts = orderExpDao.updateExpireOrders();
//    	logger.info("系统启动,处理了表中["+counts+"]个过期未支付的订单!");
//
//    	List<OrderExp> orderList = orderExpDao.selectUnPayOrders();
//    	logger.info("系统启动,发现了表中还有["+orderList.size()
//    	+"]个未到期未支付的订单!推入检查队列准备到期检查....");
//    	for(OrderExp order:orderList) {
    
    
//    		long expireTime
//    			= order.getExpireTime().getTime()-(new Date().getTime());
//    		//if(expireTime<=0){马上更新数据库}
//    		delayOrder.orderDelay(order, expireTime);
//    	}
//    }



}

架构师在设计和实现系统时需要考虑些什么?

  • 功能,这个没什么好说,实现一个应用,连基本的功能都没实现,要这个应用有何用?简直就是“一顿操作猛如虎,一看战绩零比五”

  • 高性能,能不能尽快的为用户提供服务和能为多少用户同时提供服务,性能这个东西是个很综合性的东西,从前端到后端,从架构(缓存机制、异步机制)到web容器、数据库本身再到虚拟机到算法、java代码、sql语句的编写,全部都对性能有影响。如何提升性能,要建立在充分的性能测试的基础上,然后一个个的去解决性能瓶颈。对我们今天的应用来讲,我们不想去轮询数据库,其实跟性能有非常大的关系。

  • 高可用,应用正确处理业务,服务用户的时间,这个时间当然是越长越好,希望可以7*24小时。而且哪怕服务器出现了升级,宕机等等情况下,能够以最短的时间恢复,为用户继续服务,但是实际过程中没有哪个网站可以说做到100%,不管是Google,FaceBook,阿里,腾讯,一般来说可以做到99.99%的可用性,已经是相当厉害了,这个水平大概就是一个服务在一年可以做到只有50分钟不可用。这个需要技术、资金、技术人员的水平和责任心,还要运气。

  • 高伸缩,伸缩性是指通过不断向集群中加入服务器的手段来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。就像弹簧一样挂东西一样,用户多,伸一点,用户少,,缩一点。衡量架构是否高伸缩性的主要标准就是是否可用多台服务器构建集群,是否容易向集群中添加新的服务器。加入新的服务器后是否可以提供和原来服务器无差别的服务。集群中可容纳的总的服务器数量是否有限制。

  • *高扩展,的主要标准就是在网站增加新的业务产品时,是否可以实现对现有产品透明无影响,不需要任何改动或者很少改动既有业务功能就可以上线新产品。比如购买电影票的应用,用户购买电影票,现在我们要增加一个功能,用户买了票后,随机抽取用户送限量周边。怎么做到不改动用户下订单功能的基础上增加这个功能。熟悉设计模式的同学,应该很眼熟,这是设计模式中的开闭原则(对扩展开放,对修改关闭)在架构层面的一个原则。

应用重启带来的问题

  • 保存在Queue 中的订单会丢失
  • 已过期的订单不会被处理

解决之道

在这里插入图片描述

从系统伸缩性角度考虑:应用集群化了怎么办?

集群化了会带来什么问题?应用之间会相互抢夺订单,特别是在应用重启的时候,重新启动的那个应用会把不属于自己的订单,也全部加载到自己的队列里去,一是造成内存的浪费,二来会造成订单的重复处理,而且加大了数据库的压力。

解决方案

1、 给每台服务器编号,然后在订单表里登记每条订单的服务器编号;2,更简单的,在订单表里登记每台服务器的IP地址,修改相应的sql语句即可。

几个问题:如果有一台服务器挂了怎么办?运维吃干饭的吗?服务器挂了赶紧启动啊。如果是某台服务器下线或者宕机,起不来怎么搞?这个还是还是稍微有点麻烦,需要人工干预一下,手动把库里的每条订单数据的服务器编号改为目前正常的服务器的编号,不过也就是一条sql语句的事,然后想办法让正常的服务器进行处理(重启正常的服务器)。

能不能同时解决伸缩性和扩展性问题?

用delayqueue是队列,分布式情况我们何不直接引入消息中间件呢?一举解决我们应用的伸缩性和扩展性问题

3.3 Redis有序集合

1、Redis 有序集合

Redis sortedSet 集合(sorted set 也叫zset) 是一个有序集合,每个元素(member)都关联了一个score,可以通过score排序获取集合中的值。

zset常用命令

  • 添加元素:zadd key score member[[score member]]
  • 按顺序查询元素: zrange key start stop [withscores]
  • 查询元素score: zscore key member
  • 移除元素: zrem key member 【member …】

将订单超时时间戳(long)与订单号分别设置为score与member,系统扫描第一个元素判断定是否超时,拿到分数最小的,超时时间最早的。判断与当前时间戳的关系

在这里插入图片描述
生产者

在这里插入图片描述
消费者

在这里插入图片描述

3.4 Redis Set 集合

将订单延迟时间的秒级时间戳设置为set集合的key,value 为订单ID

sadd set的key item的项值,item项可以有多个

按秒级的时间进行聚合,即 key为时间戳,里面可以由多个ID

在这里插入图片描述

3.5 ActiveMQ延迟队列

ActiveMQ的延迟和定时投递

修改配置文件(activemq.xml),增加延迟和定时投递支持

<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${
    
    activemq.data}&quot; schedulerSupport=&quot;true&quot;&gt;

需要把几个描述消息定时调度方式的参数作为属性添加到消息,broker端的调度器就会按照我们想要的行为去处理消息。

一共有4个属性

1:AMQ_SCHEDULED_DELAY :延迟投递的时间
2:AMQ_SCHEDULED_PERIOD :重复投递的时间间隔
3:AMQ_SCHEDULED_REPEAT:重复投递次数
4:AMQ_SCHEDULED_CRON:Cron表达式

ActiveMQ也提供了一个封装的消息类型:org.apache.activemq.ScheduledMessage,可以使用这个类来辅助设置,使用例子如:延迟60秒

例子:延迟30秒,投递10次,间隔10秒:

TextMessage message = session.createTextMessage("test msg");
long delay = 30 * 1000;
long period = 10 * 1000;
int repeat = 9;
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
也可使用 CRON 表达式,如message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");

代码的变化

1、 保存订单SaveOrder.java的时候,作为生产者往消息队列里推入订单,展示和修改MqProducer,这个类当然是要继承IDelayOrder

2、 消息队列会把过期订单发给消费者MqConsume,由它来负责检查订单是否已经支付和过期,来进行下一步处理。

消息队列本身又如何保证可用性和伸缩性?这个就需要ActiveMQ的集群化。

4.总结

(1) DB轮询

优点:实现简单、无 技术难点、异常恢复、支持分布式/进群环境
缺点:影响数据库性能、时效性差、效率低

(2) DelayedQueue/环形队列

优点 : 实现简单、性能较好

缺点:异常恢复困难、分布式/集群实现坤丹

(3) redis

优点:解耦、异常恢复、支持分布式/集群环境

缺点:

  • 增加redis维护、占用宽带
  • 有序 集合缺点: 当sortedSet集合中元素过多时,插入性能降低
  • Set集合缺点: 已经超时未处理的订单不好处理

史上最全的并发编程脑图:https://www.processon.com/view/5f472fd25653bb576974908f

猜你喜欢

转载自blog.csdn.net/fd2025/article/details/108528391