redis分布式锁,大家肯定不陌生,也应该都用过,主要作用是防止并发产生的数据问题,下面是一段redis锁的伪代码



public void fun() {
    try {
        String key = "";
        // 获取锁
        if (redisUtils.get(key) == null) {
            // 上锁
            redisUtils.set(key,"",expireTime);
            
            // 业务逻辑处理
            
        }else {
            // 未获得锁
            throw new RuntimeException("未获得锁");    
        }
    }finally {
        // 释放锁
        redisUtils.remove(key);
    }
}



在实际使用分布式锁还存在一个问题,我们很多方法是有事物的,如果在上述方法加了一个事物,事物是要在方法执行完之后才提交的,也就是会先释放锁,后提交事务,如果在释放锁和提交事务之间有一个线程访问了,这时候它获取到了锁,但是去查询数据库的时候还无法读取前一个事物没有提交的数据,这时候就会造成数据错乱。



我们可以使用事物钩子,可以确保在事物提交之后再去执行释放锁

TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronizationAdapter() {
    @Override
    public void afterCommit() {
        redisUtils.remove(key);
    }
});



改造后面的伪代码如下

@Transactional(rollbackFor = Exception.class)
public void fun() {
    try {
        String key = "";
        // 获取锁
        if (redisUtils.get(key) == null) {
            // 上锁
            redisUtils.set(key,"",expireTime);

            // 业务逻辑处理

        }else {
            // 未获得锁
            throw new RuntimeException("未获得锁");
        }
    }finally {
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                // 释放锁
                redisUtils.remove(key);
            }
        });
    }
}



上面的代码看似天衣无缝,但在实际使用场景中又出问题了,在执行业务代码的时候出现了各种异常(这是很正常的事情),如果出现了异常,事物是会回滚的,这时候就不会走下面的释放锁逻辑,只有等锁过期,这将是致命的。



做一点修改就可以完美的解决这个问题

@Transactional(rollbackFor = Exception.class)
public void fun() {
    try {
        String key = "";
        // 获取锁
        if (redisUtils.get(key) == null) {
            // 上锁
            redisUtils.set(key,"",expireTime);

            // 业务逻辑处理

        }else {
            // 未获得锁
            throw new RuntimeException("未获得锁");
        }
    }catch (Exception e){
        // 释放锁,抛出异常
        redisUtils.remove(key);
        throw e;
    }finally {
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                // 释放锁
                redisUtils.remove(key);
            }
        });
    }
}



PS:最开始出现这个问题的时候,我总在想是不是事物的传播行为导致事物失效了,或者是开启了新的事物,尝试了很多次,最后发现是事物回滚了。



事物钩子里全部的方法

// 挂起时出发
@Override
public void suspend() {
}

// 挂起事物抛出异常的时候会触发
@Override
public void resume() {
}

@Override
public void flush() {
}

// 在事物提交之前触发
@Override
public void beforeCommit(boolean readOnly) {
}

// 在事物完成之前触发(回滚/提交)
@Override
public void beforeCompletion() {
}

// 在事物提交之后触发
@Override
public void afterCommit() {
}

// 在事物完成之后触发 (回滚/提交 可以用status来判断)
@Override
public void afterCompletion(int status) {
}



上面我们的问题,可以有更优雅的解决办法,把 afterCommit 方法换成 afterCompletion 即可。