Backend Development 25 min read

Common Java Backend Issues and Solutions: Distributed Systems, Multithreading, and Data Access

The article distills years of Java backend experience into a concise checklist, showing how to replace JVM‑local locks with distributed locks, move thread‑blocked work to reliable message queues, model workflows with finite‑state machines, choose suitable inter‑service protocols, and always paginate queries to avoid memory and consistency problems.

Amap Tech
Amap Tech
Amap Tech
Common Java Backend Issues and Solutions: Distributed Systems, Multithreading, and Data Access

In this article the author shares practical experiences from years of working with Java backend architectures, summarizing typical problems in early‑stage companies and proposing concrete solutions.

1. System is not distributed

Single‑machine implementations using synchronized work correctly on one server but fail when the same code runs on multiple servers because the lock is only JVM‑local. Example of a single‑machine order‑grab function:

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

To make it work in a distributed environment a distributed lock is introduced:

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);
}

The author then lists the advantages (reliability, scalability, flexibility, performance, cost‑effectiveness) and disadvantages (harder troubleshooting, limited software support, higher construction cost) of distributed systems.

2. Incorrect multithreading usage

A slow interface example is a login flow that creates a new user and binds a coupon. The original implementation blocks the thread while binding the coupon:

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(phoneNumber);
}

private UserVO createNewUser(String phoneNumber) {
    UserDO user = new UserDO();
    ...
    userDAO.insert(user);
    // 绑定优惠券
    couponService.bindCoupon(user.getId(), CouponType.NEW_USER);
    return transUser(user);
}

To improve performance the coupon binding is off‑loaded to a new thread:

private UserVO createNewUser(String phoneNumber) {
    UserDO user = new UserDO();
    ...
    userDAO.insert(user);
    // 绑定优惠券
    executorService.execute(() -> couponService.bindCoupon(user.getId(), CouponType.NEW_USER));
    return transUser(user);
}

However, if the process crashes the coupon may never be bound. The author therefore recommends using an asynchronous message queue instead.

Message‑queue based solution (MetaQ) – producer:

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

Consumer:

@Slf4j
@Service
public class CouponService extends DefaultMessageListener
{
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void onReceiveMessages(MetaqMessage
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());
    }
}

The queue guarantees that if the consumer crashes the message is not acknowledged and can be retried later.

3. Poor process definition

The article introduces finite‑state machine (FSM) concepts (state, condition, action, next‑state) and applies them to a procurement workflow. Original monolithic flow:

/** 完成采购动作函数 */
public void finishPurchase(PurchaseOrder order) {
    // 完成相关处理
    ......
    // 设置完成状态
    purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue());
}

Optimized split into two independent actions:

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());
}

Using a timed job to trigger the backflow action ensures eventual consistency even if the HTTP call fails.

Further, the author discusses inter‑system communication patterns (direct DB access, Dubbo RPC, synchronous HTTP, asynchronous message queues) and lists common protocols such as HTTP/HTTPS, WebService, Dubbo/HSF, CORBA, and MetaQ.

4. Data queries without pagination

Examples of naive queries that fetch all expired orders, leading to memory overflow and timeouts. Two mitigation strategies are presented:

Limit the result set with a maximum count.

Implement proper pagination using limit #{startIndex}, #{pageSize} and expose start index / page size to callers.

Potential hidden issue of pagination when the processed rows change the query condition (e.g., status updates) is illustrated with a scheduled job that may skip rows. The author suggests using a “max count” approach or re‑querying after each batch to avoid missing records.

Overall, the article provides a checklist of best practices for Java backend development: adopt distributed locks for concurrency, prefer message queues over raw threads for reliability, model business processes with FSM, choose appropriate inter‑service communication, and always paginate large data sets.

backenddesign patternsdistributed systemsJavadatabasemultithreading
Amap Tech
Written by

Amap Tech

Official Amap technology account showcasing all of Amap's technical innovations.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.