初创公司5大Java服务困局,阿里工程师如何打破?

阿里妹导读:初创公司遇到的每一个问题都可能攸关生死。创业之初更应该总结行业的常见问题,对比方案寻找最优解。阿里巴巴地图技术专家常意在技术圈摸爬滚打数年,接触了各式各样的Java服务端架构。服务端问题见得多了,也就更能分辨出各种方案的优劣。今天,常意总结了5大初创公司存在的Java服务端难题,并尝试性地给出了一些解决方案,供大家交流参考。

1.系统不是分布式

1.1.单机版系统抢单案例


// 抢取订单函数
public synchronized void grabOrder(Long orderId, Long userId) {// 获取订单信息OrderDO order = orderDAO.get(orderId);if (Objects.isNull(order)) {throw new BizRuntimeException(String.format("订单(%s)不存在", orderId));}// 检查订单状态if (!Objects.equals(order.getStatus, OrderStatus.WAITING_TO_GRAB.getValue())) {throw new BizRuntimeException(String.format("订单(%s)已被抢", orderId));}// 设置订单被抢orderDAO.setGrabed(orderId, userId);
}

以上代码,在一台服务器上运行没有任何问题。进入函数grabOrder(抢取订单)时,利用synchronized关键字把整个函数锁定,要么进入函数前订单未被人抢取,从而抢单成功,要么进入函数前订单已被抢取导致抢单失败,绝对不会出现进入函数前订单未被抢取而进入函数后订单又被抢取的情况。

但是,如果上面的代码在两台服务器上同时运行,由于Java的synchronized关键字只在一个虚拟机内生效,所以就会导致两个人能够同时抢取一个订单,但会以最后一个写入数据库的数据为准。所以,大多数的单机版系统,是无法作为分布式系统运行的。

1.2.分布式系统抢单案例

添加分布式锁,进行代码优化:


// 抢取订单函数
public void grabOrder(Long orderId, Long userId) {Long lockId = orderDistributedLock.lock(orderId);try {grabOrderWithoutLock(orderId, userId);} finally {orderDistributedLock.unlock(orderId, lockId);}
}// 不带锁的抢取订单函数
private void grabOrderWithoutLock(Long orderId, Long userId) {// 获取订单信息OrderDO order = orderDAO.get(orderId);if (Objects.isNull(order)) {throw new BizRuntimeException(String.format("订单(%s)不存在", orderId));}// 检查订单状态if (!Objects.equals(order.getStatus, OrderStatus.WAITING_TO_GRAB.getValue())) {throw new BizRuntimeException(String.format("订单(%s)已被抢", orderId));}// 设置订单被抢orderDAO.setGrabed(orderId, userId);
}

优化后的代码,在调用函数grabOrderWithoutLock(不带锁的抢取订单)前后,利用分布式锁orderDistributedLock(订单分布式锁)进行加锁和释放锁,跟单机版的synchronized关键字加锁效果基本一样。

1.3.分布式系统的优缺点

分布式系统(Distributed System)是支持分布式处理的软件系统,是由通信网络互联的多处理机体系结构上执行任务的系统,包括分布式操作系统、分布式程序设计语言及其编译系统、分布式文件系统分布式数据库系统等。

分布式系统的优点:

  • 可靠性、高容错性:一台服务器的崩溃,不会影响其它服务器,其它服务器仍能提供服务。
  • 可扩展性:如果系统服务能力不足,可以水平扩展更多服务器。
  • 灵活性:可以很容易的安装、实施、扩容和升级系统。
  • 性能高:拥有多台服务器的计算能力,比单台服务器处理速度更快。
  • 性价比高:分布式系统对服务器硬件要求很低,可以选用廉价服务器搭建分布式集群,从而得到更好的性价比。

分布式系统的缺点:

  • 排查难度高:由于系统分布在多台服务器上,故障排查和问题诊断难度较高。
  • 软件支持少:分布式系统解决方案的软件支持较少。
  • 建设成本高:需要多台服务器搭建分布式系统。

曾经有不少的朋友咨询我:"找外包做移动应用,需要注意哪些事项?"

首先,确定是否需要用分布式系统。软件预算有多少?预计用户量有多少?预计访问量有多少?是否只是业务前期试水版?单台服务器能否解决?是否接收短时间宕机?……如果综合考虑,单机版系统就可以解决的,那就不要采用分布式系统了。因为单机版系统和分布式系统的差别很大,相应的软件研发成本的差别也很大。

其次,确定是否真正的分布式系统。分布式系统最大的特点,就是当系统服务能力不足时,能够通过水平扩展的方式,通过增加服务器来增加服务能力。然而,单机版系统是不支持水平扩展的,强行扩展就会引起一系列数据问题。由于单机版系统和分布式系统的研发成本差别较大,市面上的外包团队大多用单机版系统代替分布式系统交付。

那么,如何确定你的系统是真正意义上的分布式系统呢?从软件上来说,是否采用了分布式软件解决方案;从硬件上来说,是否采用了分布式硬件部署方案。

1.4.分布式软件解决方案

作为一个合格的分布式系统,需要根据实际需求采用相应的分布式软件解决方案。

1.4.1分布式锁

分布式锁是单机锁的一种扩展,主要是为了锁住分布式系统中的物理块或逻辑块,用以此保证不同服务之间的逻辑和数据的一致性。

目前,主流的分布式锁实现方式有3种:

  1. 基于数据库实现的分布式锁;
  2. 基于Redis实现的分布式锁;
  3. 基于Zookeeper实现的分布式锁。

1.4.2分布式消息

分布式消息中间件是支持在分布式系统中发送和接受消息的软件基础设施。常见的分布式消息中间件有ActiveMQ、RabbitMQ、Kafka、MetaQ等。

MetaQ(全称Metamorphosis)是一个高性能、高可用、可扩展的分布式消息中间件,思路起源于LinkedIn的Kafka,但并不是Kafka的一个拷贝。MetaQ具有消息存储顺序写、吞吐量大和支持本地和XA事务等特性,适用于大吞吐量、顺序消息、广播和日志数据传输等场景。

1.4.3数据库分片分组

针对大数据量的数据库,一般会采用"分片分组"策略:

分片(shard):主要解决扩展性问题,属于水平拆分。引入分片,就引入了数据路由和分区键的概念。其中,分表解决的是数据量过大的问题,分库解决的是数据库性能瓶颈的问题。

分组(group):主要解决可用性问题,通过主从复制的方式实现,并提供读写分离策略用以提高数据库性能。

1.4.4分布式计算

分布式计算( Distributed computing )是一种"把需要进行大量计算的工程数据分割成小块,由多台计算机分别计算;在上传运算结果后,将结果统一合并得出数据结论"的科学。

当前的高性能服务器在处理海量数据时,其计算能力、内存容量等指标都远远无法达到要求。在大数据时代,工程师采用廉价的服务器组成分布式服务集群,以集群协作的方式完成海量数据的处理,从而解决单台服务器在计算与存储上的瓶颈。Hadoop、Storm以及Spark是常用的分布式计算中间件,Hadoop是对非实时数据做批量处理的中间件,Storm和Spark是对实时数据做流式处理的中间件。

除此之外,还有更多的分布式软件解决方案,这里就不再一一介绍了。

1.5分布式硬件部署方案

介绍完服务端的分布式软件解决方案,就不得不介绍一下服务端的分布式硬件部署方案。这里,只画出了服务端常见的接口服务器、MySQL数据库、Redis缓存,而忽略了其它的云存储服务、消息队列服务、日志系统服务……

1.5.1一般单机版部署方案

架构说明:只有1台接口服务器、1个MySQL数据库、1个可选Redis缓存,可能都部署在同一台服务器上。

适用范围:适用于演示环境、测试环境以及不怕宕机且日PV在5万以内的小型商业应用。

1.5.2中小型分布式硬件部署方案

架构说明:通过SLB/Nginx组成一个负载均衡的接口服务器集群,MySQL数据库和Redis缓存采用了一主一备(或多备)的部署方式。

适用范围:适用于日PV在500万以内的中小型商业应用。

1.5.3大型分布式硬件部署方案

架构说明:通过SLB/Nginx组成一个负载均衡的接口服务器集群,利用分片分组策略组成一个MySQL数据库集群和Redis缓存集群。

适用范围:适用于日PV在500万以上的大型商业应用。

2.多线程使用不正确

多线程最主要目的就是"最大限度地利用CPU资源",可以把串行过程变成并行过程,从而提高了程序的执行效率。

2.1一个慢接口案例

假设在用户登录时,如果是新用户,需要创建用户信息,并发放新用户优惠券。例子代码如下:


// 登录函数(示意写法)
public UserVO login(String phoneNumber, String verifyCode) {// 检查验证码if (!checkVerifyCode(phoneNumber, verifyCode)) {throw new ExampleException("验证码错误");}// 检查用户存在UserDO user = userDAO.getByPhoneNumber(phoneNumber);if (Objects.nonNull(user)) {return transUser(user);}// 创建新用户return createNewUser(user);
}// 创建新用户函数
private UserVO createNewUser(String phoneNumber) {// 创建新用户UserDO user = new UserDO();...userDAO.insert(user);// 绑定优惠券couponService.bindCoupon(user.getId(), CouponType.NEW_USER);// 返回新用户return transUser(user);
}

其中,绑定优惠券(bindCoupon)是给用户绑定新用户优惠券,然后再给用户发送推送通知。如果随着优惠券数量越来越多,该函数也会变得越来越慢,执行时间甚至超过1秒,并且没有什么优化空间。现在,登录(login)函数就成了名副其实的慢接口,需要进行接口优化。

2.2采用多线程优化

通过分析发现,绑定优惠券(bindCoupon)函数可以异步执行。首先想到的是采用多线程解决该问题,代码如下:


// 创建新用户函数
private UserVO createNewUser(String phoneNumber) {// 创建新用户UserDO user = new UserDO();...userDAO.insert(user);// 绑定优惠券executorService.execute(()->couponService.bindCoupon(user.getId(), CouponType.NEW_USER));// 返回新用户return transUser(user);
}

现在,在新线程中执行绑定优惠券(bindCoupon)函数,使用户登录(login)函数性能得到很大的提升。但是,如果在新线程执行绑定优惠券函数过程中,系统发生重启或崩溃导致线程执行失败,用户将永远获取不到新用户优惠券。除非提供用户手动领取优惠券页面,否则就需要程序员后台手工绑定优惠券。所以,用采用多线程优化慢接口,并不是一个完善的解决方案。

2.3采用消息队列优化

如果要保证绑定优惠券函数执行失败后能够重启执行,可以采用数据库表、Redis队列、消息队列的等多种解决方案。由于篇幅优先,这里只介绍采用MetaQ消息队列解决方案,并省略了MetaQ相关配置仅给出了核心代码。

消息生产者代码:


// 创建新用户函数
private UserVO createNewUser(String phoneNumber) {// 创建新用户UserDO user = new UserDO();...userDAO.insert(user);// 发送优惠券消息Long userId = user.getId();CouponMessageDataVO data = new CouponMessageDataVO();data.setUserId(userId);data.setCouponType(CouponType.NEW_USER);Message message = new Message(TOPIC, TAG, userId, JSON.toJSONBytes(data));SendResult result = metaqTemplate.sendMessage(message);if (!Objects.equals(result, SendStatus.SEND_OK)) {log.error("发送用户({})绑定优惠券消息失败:{}", userId, JSON.toJSONString(result));}// 返回新用户return transUser(user);
}

注意:可能出现发生消息不成功,但是这种概率相对较低。

消息消费者代码:

// 优惠券服务类
@Slf4j
@Service
public class CouponService extends DefaultMessageListener<String> {// 消息处理函数@Override@Transactional(rollbackFor = Exception.class)public void onReceiveMessages(MetaqMessage<String> message) {// 获取消息体String body = message.getBody();if (StringUtils.isBlank(body)) {log.warn("获取消息({})体为空", message.getId());return;}// 解析消息数据CouponMessageDataVO data = JSON.parseObject(body, CouponMessageDataVO.class);if (Objects.isNull(data)) {log.warn("解析消息({})体为空", message.getId());return;}// 绑定优惠券bindCoupon(data.getUserId(), data.getCouponType());}
}

解决方案优点:采集MetaQ消息队列优化慢接口解决方案的优点:

  1. 如果系统发生重启或崩溃,导致消息处理函数执行失败,不会确认消息已消费;由于MetaQ支持多服务订阅同一队列,该消息可以转到别的服务进行消费,亦或等到本服务恢复正常后再进行消费。
  2. 消费者可多服务、多线程进行消费消息,即便消息处理时间较长,也不容易引起消息积压;即便引起消息积压,也可以通过扩充服务实例的方式解决。
  3. 如果需要重新消费该消息,只需要在MetaQ管理平台上点击"消息验证"即可。

3.流程定义不合理

3.1.原有的采购流程

这是一个简易的采购流程,由库管系统发起采购,采购员开始采购,采购员完成采购,同时回流采集订单到库管系统。

其中,完成采购动作的核心代码如下:


/** 完成采购动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void finishPurchase(PurchaseOrder order) {// 完成相关处理......// 回流采购单(调用HTTP接口)backflowPurchaseOrder(order);// 设置完成状态purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue());
}

由于函数backflowPurchaseOrder(回流采购单)调用了HTTP接口,可能引起以下问题:

  1. 该函数可能耗费时间较长,导致完成采购接口成为慢接口;
  2. 该函数可能失败抛出异常,导致客户调用完成采购接口失败。

3.2.优化的采购流程

通过需求分析,把"采购员完成采购并回流采集订单"动作拆分为"采购员完成采购"和"回流采集订单"两个独立的动作,把"采购完成"拆分为"采购完成"和"回流完成"两个独立的状态,更方便采购流程的管理和实现。

拆分采购流程的动作和状态后,核心代码如下:


/** 完成采购动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void finishPurchase(PurchaseOrder order) {// 完成相关处理......// 设置完成状态purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue());
}/** 执行回流动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void executeBackflow(PurchaseOrder order) {// 回流采购单(调用HTTP接口)backflowPurchaseOrder(order);// 设置回流状态purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue());
}

其中,函数executeBackflow(执行回流)由定时作业触发执行。如果回流采购单失败,采购单状态并不会修改为"已回流";等下次定时作业执行时,将会继续执行回流动作;直到回流采购单成功为止。

3.3.有限状态机介绍

3.3.1概念

有限状态机(Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的一个数学模型。

3.3.2要素

状态机可归纳为4个要素:现态、条件、动作、次态。

现态:指当前流程所处的状态,包括起始、中间、终结状态。
条件:也可称为事件;当一个条件被满足时,将会触发一个动作并执行一次状态的迁移。
动作:当条件满足后要执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。
次态:当条件满足后要迁往的状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

3.3.3状态

状态表示流程中的持久状态,流程图上的每一个圈代表一个状态。

初始状态: 流程开始时的某一状态;
中间状态: 流程中间过程的某一状态;
终结状态: 流程完成时的某一状态。

使用建议:

  1. 状态必须是一个持久状态,而不能是一个临时状态;
  2. 终结状态不能是中间状态,不能继续进行流程流转;
  3. 状态划分合理,不要把多个状态强制合并为一个状态;
  4. 状态尽量精简,同一状态的不同情况可以用其它字段表示。

3.3.4动作

动作的三要素:角色、现态、次态,流程图上的每一条线代表一个动作。

角色: 谁发起的这个操作,可以是用户、定时任务等;
现态: 触发动作时当前的状态,是执行动作的前提条件;
次态: 完成动作后达到的状态,是执行动作的最终目标。

使用建议:

  1. 每个动作执行前,必须检查当前状态和触发动作状态的一致性;
  2. 状态机的状态更改,只能通过动作进行,其它操作都是不符合规范的;
  3. 需要添加分布式锁保证动作的原子性,添加数据库事务保证数据的一致性;
  4. 类似的动作(比如操作用户、请求参数、动作含义等)可以合并为一个动作,并根据动作执行结果转向不同的状态。

4.系统间交互不科学

4.1.直接通过数据库交互

在一些项目中,系统间交互不通过接口调用和消息队列,而是通过数据库直接访问。问其原因,回答道:"项目工期太紧张,直接访问数据库,简单又快捷"。

还是以上面的采购流程为例——采购订单由库管系统发起,由采购系统负责采购,采购完成后通知库管系统,库管系统进入入库操作。采购系统采购完成后,通知库管系统数据库的代码如下:


/** 执行回流动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void executeBackflow(PurchaseOrder order) {// 完成原始采购单rawPurchaseOrderDAO.setStatus(order.getRawId(), RawPurchaseOrderStatus.FINISHED.getValue());// 设置回流状态purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue());
}

其中,通过rawPurchaseOrderDAO(原始采购单DAO)直接访问库管系统的数据库表,并设置原始采购单状态为已完成。

一般情况下,直接通过数据访问的方式是不会有问题的。但是,一旦发生竞态,就会导致数据不同步。有人会说,可以考虑使用同一分布式锁解决该问题。是的,这种解决方案没有问题,只是又在系统间共享了分布式锁。

直接通过数据库交互的缺点:

  1. 直接暴露数据库表,容易产生数据安全问题;
  2. 多个系统操作同一数据库表,容易造成数据库表数据混乱;
  3. 操作同一个数据库表的代码,分布在不同的系统中,不便于管理和维护;
  4. 具有数据库表这样的强关联,无法实现系统间的隔离和解耦。

4.2.通过Dubbo接口交互

由于采购系统和库管系统都是内部系统,可以通过类似Dubbo的RPC接口进行交互。

库管系统代码:


/** 采购单服务接口 */
public interface PurchaseOrderService {/** 完成采购单函数 */public void finishPurchaseOrder(Long orderId);
}
/** 采购单服务实现 */
@Service("purchaseOrderService")
public class PurchaseOrderServiceImpl implements PurchaseOrderService {/** 完成采购单函数 */@Override@Transactional(rollbackFor = Exception.class)public void finishPurchaseOrder(Long orderId) {// 相关处理...// 完成采购单purchaseOrderService.finishPurchaseOrder(order.getRawId());}
}

其中,库管系统通过Dubbo把PurchaseOrderServiceImpl(采购单服务实现)以PurchaseOrderService(采购单服务接口)定义的接口服务暴露给采购系统。这里,省略了Dubbo开发服务接口相关配置。

采购系统代码:


/** 执行回流动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void executeBackflow(PurchaseOrder order) {// 完成采购单purchaseOrderService.finishPurchaseOrder(order.getRawId());// 设置回流状态purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue());
}

其中,purchaseOrderService(采购单服务)为库管系统PurchaseOrderService(采购单服务)在采购系统中的Dubbo服务客户端存根,通过该服务调用库管系统的服务接口函数finishPurchaseOrder(完成采购单函数)。

这样,采购系统和库管系统自己的强关联,通过Dubbo就简单地实现了系统隔离和解耦。当然,除了采用Dubbo接口外,还可以采用HTTPS、HSF、WebService等同步接口调用方式,也可以采用MetaQ等异步消息通知方式。

4.3常见系统间交互协议

4.3.1同步接口调用

同步接口调用是以一种阻塞式的接口调用机制。常见的交互协议有:

  1. HTTP/HTTPS接口;
  2. WebService接口;
  3. Dubbo/HSF接口;
  4. CORBA接口。

4.3.2异步消息通知

异步消息通知是一种通知式的信息交互机制。当系统发生某种事件时,会主动通知相应的系统。常见的交互协议有:

  1. MetaQ的消息通知;
  2. CORBA消息通知。

4.4.常见系统间交互方式

4.4.1请求-应答

适用范围:适合于简单的耗时较短的接口同步调用场景,比如Dubbo接口同步调用。

4.4.2通知-确认

适用范围:适合于简单的异步消息通知场景,比如MetaQ消息通知。

4.4.3请求-应答-查询-返回

适用范围:适合于复杂的耗时较长的接口同步调用场景,比如提交作业任务并定期查询任务结果。

4.4.4请求-应答-回调

适用范围:适合于复杂的耗时较长的接口同步调用和异步回调相结合的场景,比如支付宝的订单支付。

4.4.5请求-应答-通知-确认

适用范围:适合于复杂的耗时较长的接口同步调用和异步消息通知相结合的场景,比如提交作业任务并等待完成消息通知。

4.4.6通知-确认-通知-确认

适用范围:适合于复杂的耗时较长的异步消息通知场景。

5.数据查询不分页

在数据查询时,由于未能对未来数据量做出正确的预估,很多情况下都没有考虑数据的分页查询。

5.1.普通查询案例

以下是查询过期订单的代码:


/** 订单DAO接口 */
public interface OrderDAO {/** 查询过期订单函数 */@Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day)")public List<OrderDO> queryTimeout();
}/** 订单服务接口 */
public interface OrderService {/** 查询过期订单函数 */public List<OrderVO> queryTimeout();
}

当过期订单数量很少时,以上代码不会有任何问题。但是,当过期订单数量达到几十万上千万时,以上代码就会出现以下问题:

  1. 数据量太大,导致服务端的内存溢出;
  2. 数据量太大,导致查询接口超时、返回数据超时等;
  3. 数据量太大,导致客户端的内存溢出。

所以,在数据查询时,特别是不能预估数据量的大小时,需要考虑数据的分页查询。

这里,主要介绍"设置最大数量"和"采用分页查询"两种方式。

5.2设置最大数量

"设置最大数量"是一种最简单的分页查询,相当于只返回第一页数据。例子代码如下:


/** 订单DAO接口 */
public interface OrderDAO {/** 查询过期订单函数 */@Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit 0, #{maxCount}")public List<OrderDO> queryTimeout(@Param("maxCount") Integer maxCount);
}/** 订单服务接口 */
public interface OrderService {/** 查询过期订单函数 */public List<OrderVO> queryTimeout(Integer maxCount);
}

适用于没有分页需求、但又担心数据过多导致内存溢出、数据量过大的查询。

5.3采用分页查询

"采用分页查询"是指定startIndex(开始序号)和pageSize(页面大小)进行数据查询,或者指定pageIndex(分页序号)和pageSize(页面大小)进行数据查询。例子代码如下:


/** 订单DAO接口 */
public interface OrderDAO {/** 统计过期订单函数 */@Select("select count(*) from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day)")public Long countTimeout();/** 查询过期订单函数 */@Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit #{startIndex}, #{pageSize}")public List<OrderDO> queryTimeout(@Param("startIndex") Long startIndex, @Param("pageSize") Integer pageSize);
}/** 订单服务接口 */
public interface OrderService {/** 查询过期订单函数 */public PageData<OrderVO> queryTimeout(Long startIndex, Integer pageSize);
}

适用于真正的分页查询,查询参数startIndex(开始序号)和pageSize(页面大小)可由调用方指定。

5.4分页查询隐藏问题

假设,我们需要在一个定时作业(每5分钟执行一次)中,针对已经超时的订单(status=5,创建时间超时30天)进行超时关闭(status=10)。实现代码如下:


/** 订单DAO接口 */
public interface OrderDAO {/** 查询过期订单函数 */@Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit #{startIndex}, #{pageSize}")public List<OrderDO> queryTimeout(@Param("startIndex") Long startIndex, @Param("pageSize") Integer pageSize);/** 设置订单超时关闭 */@Update("update t_order set status = 10 where id = #{orderId} and status = 5")public Long setTimeoutClosed(@Param("orderId") Long orderId)
}/** 关闭过期订单作业类 */
public class CloseTimeoutOrderJob extends Job {/** 分页数量 */private static final int PAGE_COUNT = 100;/** 分页大小 */private static final int PAGE_SIZE = 1000;/** 作业执行函数 */@Overridepublic void execute() {for (int i = 0; i < PAGE_COUNT; i++) {// 查询处理订单List<OrderDO> orderList = orderDAO.queryTimeout(i * PAGE_COUNT, PAGE_SIZE);for (OrderDO order : orderList) {// 进行超时关闭......orderDAO.setTimeoutClosed(order.getId());}// 检查处理完毕if(orderList.size() < PAGE_SIZE) {break;}}}
}

粗看这段代码是没有问题的,尝试循环100次,每次取1000条过期订单,进行订单超时关闭操作,直到没有订单或达到100次为止。但是,如果结合订单状态一起看,就会发现从第二次查询开始,每次会忽略掉前startIndex(开始序号)条应该处理的过期订单。这就是分页查询存在的隐藏问题:

当满足查询条件的数据,在操作中不再满足查询条件时,会导致后续分页查询中前startIndex(开始序号)条满足条件的数据被跳过。

可以采用"设置最大数量"的方式解决,代码如下:


/** 订单DAO接口 */
public interface OrderDAO {/** 查询过期订单函数 */@Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit 0, #{maxCount}")public List<OrderDO> queryTimeout(@Param("maxCount") Integer maxCount);/** 设置订单超时关闭 */@Update("update t_order set status = 10 where id = #{orderId} and status = 5")public Long setTimeoutClosed(@Param("orderId") Long orderId)
}/** 关闭过期订单作业(定时作业) */
public class CloseTimeoutOrderJob extends Job {/** 分页数量 */private static final int PAGE_COUNT = 100;/** 分页大小 */private static final int PAGE_SIZE = 1000;/** 作业执行函数 */@Overridepublic void execute() {for (int i = 0; i < PAGE_COUNT; i++) {// 查询处理订单List<OrderDO> orderList = orderDAO.queryTimeout(PAGE_SIZE);for (OrderDO order : orderList) {// 进行超时关闭......orderDAO.setTimeoutClosed(order.getId());}// 检查处理完毕if(orderList.size() < PAGE_SIZE) {break;}}}
}


双11福利来了!先来康康#怎么买云服务器最便宜# [并不简单]参团购买指定配置云服务器仅86元/年,开团拉新享三重礼:1111红包+瓜分百万现金+31%返现,爆款必买清单,还有iPhone 11 Pro、卫衣、T恤等你来抽,马上来试试手气👉  https://www.aliyun.com/1111/2019/home?utm_content=g_1000083110

原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/517537.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Navicat for MySQL连接MySQL数据库时各种错误解决

一 、2058错误 通过命令行进入MySQL&#xff0c;执行如下命令&#xff1a; ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY root截图&#xff1a; 二、1251错误 通过命令行进入MySQL&#xff0c;执行如下命令&#xff1a; ALTER USER rootlocalhost I…

构建灵活工作方式 戴尔最智能商用客户端产品线全面上市

戴尔Latitude、Precision和OptiPlex产品组合再次全面升级&#xff0c;带来更高水平生产力 2020年6月12日&#xff0c;随着“新基建”首次被写入《政府工作报告》&#xff0c;数字经济将成为释放经济新动能的最重要途径。在企业加速数字化转型的过程中&#xff0c;生产力转型是…

秒杀场景_解决秒杀超卖问题_04

文章目录一、商品微服务改造二、秒杀微服务改造2.1. SkillGoodService 改造2.2. MutilThreadOrder 改造一、商品微服务改造 SkillGoodService改造 package com.gblfy.service;import com.gblfy.dao.SkillGoodRepository; import com.gblfy.entity.SkillGood; import lombok.R…

初识 Knative: 跨平台的 Serverless 编排框架

Knative 是什么 Knative 是 Google 在 2018 的 Google Cloud Next 大会上发布的一款基于 Kubernetes 的 Serverless 框架。Knative 一个很重要的目标就是制定云原生、跨平台的 Serverless 编排标准。Knative 是通过整合容器构建(或者函数)、工作负载管理(和动态扩缩)以及事件模…

python使用PyMysql连接MySQL实现增删改查

文章目录一、安装PyMysql&#xff1a;1.方式一&#xff1a;使用命令行2.方式二&#xff1a;通过PyCharm编译器二、操作MySQL数据库步骤1. 使用import导入相应的类2.获得数据库的连接3.创建游标对象4.执行SQL语句5.关闭数据库连接三、案例1. 查询单条数据2. 查询多条数据3.创建数…

一分钟解决 Github 访问慢

文章目录1. 获取域名对应的ip2. 复制ip3. 配置hostsgithub.com对应的ip有多个选一个响应快的即可&#xff0c;但是一直在变&#xff0c;用的时候&#xff0c;设置一次即可 1. 获取域名对应的ip https://tool.chinaz.com/dns/?type1&hostgithub.com&ip 2. 复制ip 3. …

做一个高一致性、高性能的Flutter动态渲染,真的很难么?

Flutter动态模板渲染架构升级 ​ 最近小组在尝试使用集团DinamicX的DSL&#xff0c;通过下发DSL模板&#xff0c;实现Flutter端的动态化模板渲染。我们解决了性能方面的问题后&#xff0c;又面临了一个新的挑战——渲染一致性。我们该如何在不降低渲染性能的前提下&#xff0c…

数据科学产业中哪些架构最热门?本文为你盘点了 5 款!

作者 | Sai Krishna译者 | 火火酱&#xff0c;责编 | Carol封图 | CSDN 付费下载自视觉中国地球上的数据量每分每秒都在增加&#xff0c;海量的数据源源不断地从四面八方涌入各种机构组织&#xff0c;而这些数据最终或许会成为能够指引我们做出战略决策的宝贵财富。这就是数据科…

面向云原生的混沌工程工具-ChaosBlade

作者 | 肖长军&#xff08;穹谷&#xff09;阿里云智能事业群技术专家 导读&#xff1a;随着云原生系统的演进&#xff0c;如何保障系统的稳定性受到很大的挑战&#xff0c;混沌工程通过反脆弱思想&#xff0c;对系统注入故障&#xff0c;提前发现系统问题&#xff0c;提升系…

centOS docker容器的安装

文章目录一、docker安装步骤1.卸载旧版本2.安装依赖3.添加镜像源4.查看仓库中的所有docker版本5.安装docker6.启动docker7.设置开机自启动docker8.查看docker是否安装成功9.卸载docker一、docker安装步骤 提示&#xff1a;安装之前可以使用命令&#xff1a;yum check-update检查…

泡着枸杞写bug的三流程序员凭什么逆袭到一线大厂?

大多数互联网的从业者都有一个梦想&#xff1a;进大厂。因为不仅可以享受较好的福利待遇&#xff0c;也能与更优秀的人一起共事&#xff0c;获得更专业、更快速的成长。最近经常有朋友提及想要入门编程学习&#xff0c;该如何学习&#xff1f;关于编程学习&#xff0c;各种语言…

哪种人是软件设计中的稀缺型人才?

阿里妹导读&#xff1a;好的系统架构离不开好的接口设计&#xff0c;因此&#xff0c;真正懂接口设计的人往往是软件设计队伍中的稀缺型人才。 为什么在接口制定标准中说&#xff1a;一流的企业做标准&#xff0c;二流的企业做品牌&#xff0c;三流的企业做产品&#xff1f;依赖…

快速Ubuntu的配置(以ubuntu 20.04桌面版为例)

文章目录一、&#x1f496;更换镜像源二、&#x1f496;安装VMware Tools工具三、&#x1f496;安装Google浏览器四、&#x1f496;安装搜狗输入法五、&#x1f496;安装C/C开发工具Clion&#x1f340;1.安装编译环境&#x1f340;2.安装开发环境一、&#x1f496;更换镜像源 …

阿里工程师太凶残了,竟把服务器泡在“水里”!

万众瞩目的第11个双11要来了&#xff01;这次天猫说要帮大家省500亿&#xff0c;身边加购物车、组队盖楼的同学数不胜数&#xff0c;热闹非凡。 但在阿里&#xff0c;有一位低调的“高冷男”&#xff0c;丝毫不为红红火火的双11所动&#xff0c;依然稳如磐石、淡定自若。 他的…

“我今年 31 岁,工作 7 年,明年退休...”

凌晨 1:30 的北京&#xff0c;商务楼静静地伫立着&#xff0c;街道上的车水马龙&#xff0c;慢慢地停歇了&#xff0c;只有他的工作台灯还没有休息&#xff0c;台灯忽明忽暗地坚持着。凌晨 2:00 的闹钟忍不住提醒他&#xff0c;电脑也偷偷跳出窗口&#xff1a;“主人&#xff0…

据说这是双11前互联网人的一天~

双11前&#xff0c;据说互联网人的一天是这样度过.... 6段故事&#xff0c;6个黑话关键词&#xff0c;生动刻画了双11前互联网人的一天&#xff01; 上午9点&#xff0c;运营找老板汇报双11方案。 运营说&#xff1a;老板&#xff0c;本次营销方案你看需要一些预算资源支持&a…

快速入门docker容器

文章目录&#x1f332;1.拉取一个镜像&#x1f332;2. 在docker里启动一个镜像&#x1f332;3.查看所有的镜像列表&#x1f332;4.删除镜像&#x1f332;5.删除全部image镜像&#x1f332;6.运行tomcat&#x1f332;7.删除所有未运行的容器&#x1f332;8.以守护态运行容器&…

不会玩游戏的程序员不是好作家,《深入理解Java虚拟机》周志明来了!

嘉宾&#xff1a;周志明、杨福川采访、撰文&#xff1a;Satoh_AI这次采访起源来自于我和豆瓣的一位读者有同样的好奇心&#xff0c;为什么网上搜不到周志明老师的更多信息&#xff1f;为什么“80后玩家”可以把本本书都维持在9.0分左右&#xff1f;他的“社恐”到底有多严重?所…

当手机淘宝遇见折叠屏,让购物更随心

华为 5G 新品发布会上&#xff0c;Mate X 正式亮相&#xff0c;淘宝也作为重点展示应用出现在发布会的 PPT 上&#xff0c;同时也成为折叠屏生态联盟应用矩阵的第一位。 现场华为折叠屏上的淘宝多任务演示 伴随手淘技术团队对华为折叠屏适配工作的展开。半年前还只是概念方案的…

SpringBoot2.x 整合 Ueditor

文章目录一、基础准备1. 创建项目并引入依赖2. 下载Ueditor源码3. Java代码整合4. 静态文件整合二、静态页面控制层2.1. index.html2.2. demo1.html2.3. demo2.html2.4. demo3.html2.5. Controller三、配置调整3.1. 图片大小3.2. 修改ueditor.config.js3.3. 修改config.json文件…