Khóa Phân Tán Giải Quyết Vấn Đề Một Người Một Đơn Hàng

  1. Hạn chế của khóa đồng bộ hóa (synchronized) trong môi trường phân tán ========================================

Khóa đồng bộ hóa là khóa tại JVM

Khi có hai luồng cùng truy cập vào khối được khóa, khóa đồng bộ hóa này sẽ trở nên vô hiệu. Điều này xảy ra vì synchronized là khóa cục bộ, chỉ có khả năng đồng bộ hóa ở cấp độ luồng, mỗi JVM đều có một khóa riêng biệt, không thể khóa qua nhiều JVM. Khi một luồng vào phương thức hoặc khối mã được khóa bởi từ khóa synchronized, nó sẽ cố gắng lấy khóa đối tượng (còn gọi là khóa giám sát). Nếu khóa không bị luồng khác chiếm giữ, luồng hiện tại sẽ nhận được khóa và có thể tiếp tục thực thi mã; nếu không, luồng hiện tại sẽ vào trạng thái chặn cho đến khi nhận được khóa.

Bây giờ chúng ta đã tạo hai node, tức là có hai JVM, do đó synchronized sẽ trở nên vô hiệu!

Cải tiến: Sử dụng khóa phân tán

  • Lý do khóa synchronized không hiệu quả là vì mỗi JVM đều có một khóa giám sát riêng để theo dõi khóa synchronized trong JVM đó,
  • Do đó không thể đảm bảo chỉ có một luồng truy cập vào một khối mã trong cụm phân tán. Vì vậy chúng ta sẽ sử dụng một khóa phân tán.
  1. Giải quyết vấn đề một người một đơn hàng bằng khóa phân tán ============================================

  2. Định nghĩa giao diện khóa ILock trong package công cụ


package com.hmdp.utils;

public interface ILock {
    boolean tryLock(long timeoutSec);

    void unLock();
}
  1. Triển khai logic khóa phân tán trong SimpleRedisLock.java

2.1 Thuộc tính khóa bao gồm tên khóa và lớp StringRedisTemplate của Spring

2.2 Phương thức tryLock

  1. Lấy ID luồng
  2. Khóa Redis là tên khóa
  3. Giá trị Redis là ID luồng

2.3 Phương thức unLock

Để Redis xóa khóa có tên khóa.

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {
    private final StringRedisTemplate redisTemplate;
    private final String keyName;

    public SimpleRedisLock(StringRedisTemplate redisTemplate, String keyName) {
        this.redisTemplate = redisTemplate;
        this.keyName = keyName;
    }

    @Override
    public boolean tryLock(long timeoutSeconds) {
        String threadIdentifier = Thread.currentThread().getId() + "";
        Boolean result = redisTemplate.opsForValue().setIfAbsent("lock:" + keyName, threadIdentifier, timeoutSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }

    @Override
    public void unLock() {
        redisTemplate.delete("lock:" + keyName);
    }
}
  1. Sử dụng khóa phân tán

  1. Dựa vào voucherId được truyền vào, truy vấn phiếu giảm giá
  2. Kiểm tra xem phiếu giảm giá có hợp lệ không
  • Tương tác với MySql, so sánh thời gian bắt đầu, thời gian kết thúc và thời gian hiện tại
  • Kiểm tra xem số lượng tồn kho có nhỏ hơn 1 không
  1. Lấy userId từ ThreadLocal
  2. Gọi khóa phân tán để khóa, khóa có tên là userId, và đặt thời gian hết hạn
  3. Thực hiện thao tác tạo đơn hàng trước khi try, và giải phóng khóa trong finally.
/**
 * Sử dụng Redis làm khóa phân tán, các kiểm tra hợp lệ trước đó vẫn giữ nguyên.
 *
 * @param voucherId
 * @return
 */
@Transactional
public Result seckillVoucherRedis(Long voucherId) {
    // 1. Truy vấn phiếu giảm giá
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    // 2. Kiểm tra xem phiếu giảm giá có hợp lệ không
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        // Chưa bắt đầu đếm ngược
        return Result.fail("Chưa bắt đầu đếm ngược");
    }
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.fail("Đếm ngược đã kết thúc");
    }
    if (voucher.getStock() < 1) {
        // Hết phiếu
        return Result.fail("Phiếu giảm giá đã được hết");
    }
    // 3. Sử dụng khóa phân tán để tạo đơn hàng
    Long userId = UserHolder.getUser().getId();
    SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
    boolean lockAcquired = lock.tryLock(1200);
    // Không lấy được khóa
    if (!lockAcquired) {
        return Result.fail("Một người chỉ được đặt một đơn");
    }
    try {
        // Lấy khóa thành công, tạo đối tượng proxy, sử dụng đối tượng proxy để gọi phương thức giao dịch bên thứ ba
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(userId, voucherId);
    } finally {
        lock.unLock();
    }
}

Lưu ý quan trọng Sử dụng try...finally... để đảm bảo khóa được giải phóng khi có ngoại lệ, lưu ý không sử dụng catch ở đây, phương thức giao dịch A gọi phương thức giao dịch B, phương thức giao dịch A không thể catch trực tiếp, nếu không sẽ dẫn đến giao dịch bị vô hiệu hóa.

Thẻ: Redis Khóa Phân Tán Hệ thống phân tán Quản Lý Đơn Hàng

Đăng vào ngày 3 tháng 6 lúc 22:09