设计一个利用事务特性可以阻塞线程的排他锁,并且通过注解和 AOP 来实现

设计思路:

利用数据库表记录锁标识:通过唯一标识符(如方法名 + 参数),我们可以在数据库中插入一条记录,表示当前方法正在执行。这条记录需要记录插入时间。

注解:通过注解标识哪些方法需要加锁,我们可以动态生成唯一标识符。

AOP:使用 AOP 切面处理方法调用逻辑,确保在进入目标方法之前判断是否已经有其他线程持有锁。如果有,则阻塞当前线程,直到锁被释放。

事务:利用事务的隔离性,保证在同一时刻,只有一个线程能够执行某个方法,其他线程需要等待。

超时检测:如果在某个时间点锁没有被释放(例如,锁存在时间超过10分钟),则认为发生了死锁,自动清理相关记录。

步骤及代码实现

  1. 数据库表设计
    首先,我们需要一个数据库表来存储锁的状态。表的结构大致如下:
java 复制代码
CREATE TABLE method_lock (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    lock_key VARCHAR(255) NOT NULL, -- 锁的唯一标识(方法名+参数)
    created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 锁的创建时间
    updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 最后更新时间
    status VARCHAR(50) DEFAULT 'LOCKED', -- 锁的状态,LOCKED 表示锁定,RELEASED 表示释放
    expired BOOLEAN DEFAULT FALSE -- 是否过期,10分钟未释放的锁算死锁
);
  1. 创建锁的注解
    接下来,我们定义一个注解来标识需要加锁的方法。
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodLock {
    String value(); // 锁的标识(如方法名 + 参数的唯一标识符)
}
  1. 实现 AOP 切面
    在切面中,我们要通过 @Around 来拦截带有 @MethodLock 注解的方法,获取方法的唯一标识,并在数据库中插入或查询锁的状态。
java 复制代码
@Aspect
@Component
public class MethodLockAspect {

    @Autowired
    private LockRepository lockRepository; // 用于查询和操作数据库的 repository

    @Around("@annotation(methodLock)") // 拦截带有 @MethodLock 注解的方法
    public Object around(ProceedingJoinPoint joinPoint, MethodLock methodLock) throws Throwable {
        String lockKey = methodLock.value() + getMethodSignature(joinPoint); // 生成锁的唯一标识符

        // 获取当前时间戳
        long currentTime = System.currentTimeMillis();

        // 尝试获取锁
        if (tryAcquireLock(lockKey, currentTime)) {
            try {
                // 如果获取到锁,则执行目标方法
                return joinPoint.proceed();
            } finally {
                // 目标方法执行完毕后释放锁
                releaseLock(lockKey);
            }
        } else {
            // 如果无法获取到锁,则阻塞当前线程或者抛出异常
            throw new RuntimeException("Unable to acquire lock, try again later.");
        }
    }

    private boolean tryAcquireLock(String lockKey, long currentTime) {
        // 查询数据库中是否存在该锁记录
        LockEntity lockEntity = lockRepository.findByLockKey(lockKey);

        if (lockEntity == null) {
            // 如果不存在锁记录,则插入新的锁记录
            LockEntity newLockEntity = new LockEntity();
            newLockEntity.setLockKey(lockKey);
            newLockEntity.setCreatedTime(new Timestamp(currentTime));
            lockRepository.save(newLockEntity);
            return true;
        } else {
            // 如果锁存在,检查锁是否已经过期(超过10分钟)
            long lockAge = currentTime - lockEntity.getCreatedTime().getTime();
            if (lockAge > 10 * 60 * 1000) {
                // 如果锁超时超过10分钟,认为是死锁,清理并重新获取锁
                lockRepository.deleteByLockKey(lockKey); // 删除死锁记录
                LockEntity newLockEntity = new LockEntity();
                newLockEntity.setLockKey(lockKey);
                newLockEntity.setCreatedTime(new Timestamp(currentTime));
                lockRepository.save(newLockEntity);
                return true;
            }
            // 如果锁没有过期,则阻塞当前线程
            return false;
        }
    }

    private void releaseLock(String lockKey) {
        // 释放锁:删除数据库中的锁记录
        lockRepository.deleteByLockKey(lockKey);
    }

    private String getMethodSignature(ProceedingJoinPoint joinPoint) {
        // 获取方法签名(例如方法名 + 参数)
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        StringBuilder signature = new StringBuilder(methodName);
        for (Object arg : args) {
            signature.append("-").append(arg != null ? arg.toString() : "null");
        }
        return signature.toString();
    }
}
  1. LockRepository 示例
    假设使用的是 Spring Data JPA 来进行数据库操作,我们可以定义一个简单的 Repository 来操作 method_lock 表。
java 复制代码
public interface LockRepository extends JpaRepository<LockEntity, Long> {
    LockEntity findByLockKey(String lockKey);
    void deleteByLockKey(String lockKey);
}
  1. LockEntity 实体类
    LockEntity 对应数据库中的 method_lock 表:
java 复制代码
@Entity
@Table(name = "method_lock")
public class LockEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String lockKey;

    @Column(name = "created_time")
    private Timestamp createdTime;

    @Column(name = "updated_time")
    private Timestamp updatedTime;

    private String status;

    private Boolean expired;

    // Getters and Setters
}
  1. 使用示例
    现在,我们可以在需要加锁的方法上使用 @MethodLock 注解,示例如下:
java 复制代码
@Service
public class MyService {

    @MethodLock("uniqueLockKey")
    @Transactional
    public void executeTask(String param) {
        // 执行需要加锁的逻辑
        System.out.println("Executing task with param: " + param);
    }
}

关键点总结:

锁的唯一标识:通过方法名和参数生成一个唯一的标识符 lockKey。

数据库表记录锁:通过表记录锁的状态,保证只有一个线程能获取到锁。

AOP 和注解结合:使用 AOP 和注解对方法进行拦截,判断是否已经有其他线程在执行,若没有则获取锁,若有则阻塞或抛出异常。

事务特性:保证同一时刻,只有一个线程能够执行目标方法,其他线程需要等待,。

死锁处理:在锁存在超过 10 分钟时认为是死锁,进行清理。

通过这种方式,可以实现基于事务的排他锁,确保多个线程访问同一资源时进行排队执行

解释:

例如,多个接口调用一个方法,要求多个线程调用该方法时,这些线程排队执行,该方法使用注解,aop切面,通过该注解上的特定字符串和方法名,组成唯一标识,将该标识插入表中,并且记录插入时间,并且该标识是唯一的,当有线程调用该方法时,先组装唯一标识,查询表中是否有该标识的数据,并判断是否时间距离现在是否超过了10分钟,如果超过10分钟就是死锁了,直接删除该标识的所有记录,如果没有查到就插入一条记录,如果该方法没有执行完,就在一个事务里面,该事务没有结束,当有其他方法调用该方法时,就回去查询并插入,由于前一个事务未结束,导致当前唯一标识一致,卡在插入数据时,从而达到阻塞的目的,最后方法结束后将该数据删除。

相关推荐
一水鉴天5 分钟前
为AI聊天工具添加一个知识系统 之27 支持边缘计算设备的资源存储库及管理器
数据库·人工智能·前端框架
拾忆,想起1 小时前
Spring拦截链揭秘:如何在复杂应用中保持控制力
java·数据库·spring
Bytebase2 小时前
AWS re:Invent 2024 现场实录 - It‘s all about Scale
运维·数据库·dba·开发者·数据库管理·devops
GreatSQL2 小时前
【GreatSQL优化器-10】find_best_ref
数据库
weisian1513 小时前
Mysql--基础篇--多表查询(JOIN,笛卡尔积)
数据库·mysql
LabVIEW开发3 小时前
LabVIEW数据库管理系统
数据库·labview
NineData3 小时前
NineData云原生智能数据管理平台新功能发布|2024年12月版
数据库·sql·算法·云原生·oracle·devops·ninedata
xsh801442423 小时前
Java Spring Boot监听事件和处理事件
java·前端·数据库
焱焱枫3 小时前
Oracle Database 23ai 新特性: UPDATE 和 DELETE 语句的直接联接
数据库·oracle
kikyo哎哟喂3 小时前
InnoDB存储引擎对MVCC的实现
数据库