In those years, we've seen Java server-side "problem"

REVIEW

Mind famous master of Mr. Wang Yangming goes in the Ming Dynasty, "a teaching record":

Road no VCDs and DVDs, VCDs and DVDs have seen in person. As this room, people come in early, I saw a large scale so. At long, then the column wall and the like, one by one to understand it. For longer, as some wenzao column, and makes all saw it. However, just a room.

Yes, there really any theoretical knowledge VCDs and DVDs of the points, just different people's level of awareness. I fought for years in the start-up company, contacted a wide variety of Java server architecture, see much more naturally deep understanding, we can distinguish the pros and cons of the various options. Here, the author summarizes the Java server startups exist some problems, and attempted to give some immature solutions.

1. The system is not distributed

With the development of the Internet, the computer system has long been the transition from stand-alone work independently to multiple machines working together. Computer cluster exists in the way, build a large and complex distributed application services in accordance with the theory, it has long been popular and widely applied. However, there are still many startups software system stays in "stand-alone."

1.1. Stand-alone version of the system to grab a single case

Here, with relatively high concurrency grab a single function as an example:

// 抢取订单函数
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);
}

The code above, on a server running without any problems. When entering the function grabOrder (grab take orders), use the synchronized keyword to lock the entire function, or enter the function before the order has not been taken in order to grab people rush to take orders succeed or function before entering the order has been robbed led to grab taken fail to take orders, will not appear in the top order has not robbed function to take into the case after the function has robbed orders taken.

However, if the above code to run simultaneously on two servers, because the Java synchronized keyword only take effect within a virtual machine, so two people can simultaneously will lead to a rush to take orders, but the database is written to the last one the data subject. So, most of the stand-alone version of the system is not operating as a distributed system.

1.2. Distributed Systems grab a single case

Adding Distributed Lock, code optimization:

// 抢取订单函数
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);
}

Optimized code, call the function grabOrderWithoutLock (not locked rush to take orders) before and after, for locking and releasing the lock using a distributed lock orderDistributedLock (orders distributed lock), with the synchronized keyword lock standalone version of the basic effect same.

1.3 Advantages and disadvantages of distributed systems

Distributed Systems (Distributed System) is a software system to support distributed processing is performed by the system on a multi-processor architecture communications networking tasks, including distributed operating systems, distributed programming language and compiler system, distribution file system distributed database systems.

Distributed system advantages:

  1. Reliability, fault tolerance:

The collapse of one server will not affect other servers, other servers can still provide service.

  1. Scalability:

If the system service capabilities, additional servers can scale horizontally.

  1. flexibility:

Can be easily installed, embodiments, expansion and upgrade the system.

  1. High performance:

It has more than one server computing power, faster processing speed than a single server.

  1. Cost-effective:

Distributed system server hardware requirements are very low, you can use to build distributed low-cost server clusters, resulting in better value for money.

Distributed system disadvantages:

  1. Troubleshoot high degree of difficulty:

Because the system is distributed across multiple servers, troubleshooting and problem diagnosis more difficult.

  1. Software support less:

Distributed systems software solutions to support less.

  1. High construction costs:

Require multiple servers to build distributed systems.

There was a lot of my friends advice: "? Looking for outsourcing do mobile applications, need to pay attention to what matters."

First, determine the need for a distributed system. How many software budgets? How many users are expected to amount? How much traffic is expected? Is it just to test the water business early version? A single server can be resolved? Whether to receive a short downtime? ...... if taken together, stand-alone system can be resolved, then do not employ a distributed system. Because very different stand-alone and distributed systems, the difference corresponding software development costs are also high.

Second, to determine whether truly distributed system. The greatest feature of distributed systems, service is when there is insufficient system capacity, through horizontal expansion of way, by adding servers to increase service capacity. However, the stand-alone version of the system does not support the level of expansion, forced the expansion will lead to a series of data problems. Since R & D cost difference stand-alone and distributed systems is large, the market most of the outsourcing team in place to deliver a distributed system with a stand-alone system. So, how to make sure your system is a distributed system in the true sense of it? From the software, whether to adopt a distributed software solution ; it is from the hardware, whether to adopt a distributed hardware deployment scenarios .

1.4. Distributed Software Solutions

Qualified as a distributed system, need to adopt appropriate distributed software solutions based on actual demand.

1.4.1. Distributed Lock

Distributed single lock is a lock extension, mainly in order to lock the physical or logical blocks of a distributed system, with in order to ensure consistency between the different service logic and data.

At present, the mainstream of distributed lock implementations have three kinds:

  1. Distributed Lock database implementations;
  2. Distributed lock Redis implementation;
  3. Zookeeper distributed lock-based implementation.

1.4.2. Distributed Message

Distributed messaging middleware to support send and receive messages in a distributed system software infrastructure. Common distributed messaging middleware has ActiveMQ, RabbitMQ, Kafka, MetaQ and so on.

MetaQ (Metamorphosis full name) is a high-performance, highly available, scalable, distributed messaging middleware, the idea originated in Kafka LinkedIn, but not a copy of Kafka. MetaQ message having sequential write storage, throughput and large local and XA transaction support features such as, for large throughput, the order message, the broadcast data transmission and log the scene.

1.4.3 Database packet fragmentation

Database for the large amount of data, usually a "slice group" strategy:

Fragment (Shard) : Main solve the scalability problem, belonging to the horizontal resolution. Introducing fragments, it introduces the concept of data routing and partitioning keys. Among them, sub-table to solve the problem of excessive data, sub-library solve the problem of database performance bottlenecks.

Group (Group) : the availability of the main problems to solve, by the replication master implementations, and providing separate read and write strategy for improving database performance.

1.4.4. Distributed Computing

Distributed Computing (Distributed computing) is a kind of "the need for compute-intensive engineering data into smaller pieces, calculated by multiple computers respectively; after uploading the result of the operation, the result of a consolidation of data drawn conclusions" of science.

The current high-performance server when processing huge amounts of data, computing power, memory capacity and other indicators are far unable to meet the requirements. In the era of big data, engineers use cheap servers distributed services cluster to cluster in a collaborative manner to complete the processing of massive data, so as to solve the bottleneck of a single server and stored on a computer. Hadoop, Storm and Spark is a common distributed computing middleware, Hadoop is a non-real-time data to do batch processing middleware, Storm and Spark is doing for real-time streaming data middleware.

In addition, there are more distributed software solutions, there is no longer introduced one by one.

1.5. Distributed hardware deployment

Introduction to the service side of the distributed software solutions, hardware will have to introduce a distributed server-side deployment. Here, only to draw a common server interface server, MySQL database, Redis cache, while ignoring other cloud storage services, message queues service, log system service ......

1.5.1 General stand-alone deployment

lALPDgQ9rFOLc3PNAZrNAw8_783_410_png_620x10000q90g

Architecture Description:

Only 1 station interface server, a MySQL database, an optional Redis cache may have deployed on the same server.

Scope:

Suitable for presentation environment, test environment and not afraid of downtime and Japanese PV small business applications in less than 50,000.

1.5.2. SME distributed hardware deployment

lALPDgQ9rFOLc3nNAjjNA64_942_568_png_620x10000q90g

Architecture Description:

By SLB / Nginx interface composed of a load-balancing cluster of servers, MySQL database Redis cache and a main deployment using a device (or apparatus) of.

Scope:

PV is suitable for small and medium sized commercial applications in Japan within 5 million.

1.5.3. Large distributed hardware deployments

lALPDgQ9rFOLc33NAjjNBPk_1273_568_png_620x10000q90g

Architecture Description:

By SLB / Nginx interface consisting of a load-balancing server cluster, using the slice grouping strategy to form a cluster and MySQL database Redis cache clusters.

Scope:

Suitable for large-scale commercial application of PV day 500 million or more.

2. Incorrect use multithreading

Multithreading The main purpose is to "maximize the use of CPU resources" can be a serial process into a parallel process, thus improving the efficiency of the implementation of the program.

2.1 interface to a slow Case

Assume that when the user logs, if a new user, you need to create user information, and the issuance of new users coupons. Examples code is as follows:

// 登录函数(示意写法)
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);
}

Among them, bind a coupon (bindCoupon) is to bind the new user coupon user, and then give the user sends push notifications. As more and more if the number of coupons, this function will become slower and slower execution time even more than one second, and there is no room for optimization. Now, login (login) function has become a veritable slow the interface, the interface needs to be optimized.

2.2. Multi-threaded optimization

The analysis revealed that the binding coupon (bindCoupon) function can be executed asynchronously. First thought is to use multiple threads to solve this problem, the code is as follows:

// 创建新用户函数
private UserVO createNewUser(String phoneNumber) {
    // 创建新用户
    UserDO user = new UserDO();
    ...
    userDAO.insert(user);

    // 绑定优惠券
    executorService.execute(()->couponService.bindCoupon(user.getId(), CouponType.NEW_USER));
    
    // 返回新用户
    return transUser(user);
}

Now, to perform the binding coupon (bindCoupon) function in the new thread, allowing users to login (login) function performance is greatly improved. However, if you perform the binding process coupons function in the new thread, phylogenetic reboot or crash causes the thread execution fails, the user will not get new users coupon forever. Unless the user manually receive a coupon page, or to require the programmer to manually bind coupons background. Therefore, to optimize slow interfaces with multi-threading, it is not a perfect solution.

2.3 Optimization of the message queue using

If you want to ensure that the binding coupons can restart function fails to perform, you can use the database tables, Redis queues, message queues, and other solutions. Due to space priority, here only the message queue using MetaQ solution, is omitted and only the configuration given MetaQ core code.

News producer Code:

// 创建新用户函数
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);
}

Note: The possible occurrence of the message was unsuccessful, but the probability is relatively low.

消息消费者代码:

// 优惠券服务类
@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.原有的采购流程

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

lADPDgQ9rFOLc35izQI6_570_98_jpg_620x10000q90g

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

/** 完成采购动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void finishPurchase(PurchaseOrder order) {
    // 完成相关处理
    ......

    // 回流采购单(调用HTTP接口)
    backflowPurchaseOrder(order);
    
    // 设置完成状态
    purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue());
}

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

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

3.2.优化的采购流程

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

lADPDgQ9rFOLc39izQMm_806_98_jpg_620x10000q90g

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

/** 完成采购动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
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个要素:现态、条件、动作、次态。

lADPDgQ9rFOLc4LMqc0BTg_334_169_jpg_620x10000q90g

现态:指当前流程所处的状态,包括起始、中间、终结状态。

条件:也可称为事件;当一个条件被满足时,将会触发一个动作并执行一次状态的迁移。

动作:当条件满足后要执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。

次态:当条件满足后要迁往的状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

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.请求-应答

lADPDgQ9rFOLc4TNATnNAXA_368_313_jpg_620x10000q90g

适用范围:

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

4.4.2.通知-确认

lADPDgQ9rFOLc4XNAQXNAUw_332_261_jpg_620x10000q90g

适用范围:

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

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

lADPDgQ9rFOLc4jNAeLNAUs_331_482_jpg_620x10000q90g

适用范围:

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

4.4.4.请求-应答-回调

lADPDgQ9rFOLc5DNAarNAUw_332_426_jpg_620x10000q90g
适用范围:

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

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

lADPDgQ9rFOLc5PNAarNAUw_332_426_jpg_620x10000q90g

适用范围:

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

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

lADPDgQ9rFOLc5XNAajNAUw_332_424_jpg_620x10000q90g

适用范围:

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

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.3.分页查询隐藏问题

假设,我们需要在一个定时作业(每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;
    /** 作业执行函数 */
    @Override
    public 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;
    /** 作业执行函数 */
    @Override
    public 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;
            }
        }
    }
}

后记

本文是《那些年,我们见过的Java服务端“乱象”》的姐妹篇,前文主要介绍的是Java服务端规范上的问题,而本文主要介绍的是Java服务端方案上的问题。

Like this document to "KK carpool" "E-generation drive" under the team that year, missed once fought with brothers, nostalgia for the period of years on behalf driving drivers return late at night escort. Deeply regrets, "KK carpool" was emerging, not enough time to properly develop, the company arm was abolished. The good news is, "KK carpool" hearts of the people, is now said to have become a "civil society."

Guess you like

Origin yq.aliyun.com/articles/720137
Recommended