🍓2025年最强打破并发魔咒!用Redisson自定义注解解锁分布式锁的神秘力量~

568 阅读14分钟

一、使用背景

在这个流量为王的时代,每一次用户操作都可能触发千万级的并发访问,系统的稳定性成为每位开发者的终极战场。无论是电商大促、热门抢票还是社交平台的互动高峰,如何让你的系统在狂风暴雨中稳如泰山?一把强大而可靠的分布式锁,就是破局的利器!

想象一下:

  • 双十一秒杀现场,上亿用户同时点击“立即购买”,你的库存还能hold住吗?
  • 高并发支付系统,万千订单一键生成,你的数据还安全吗?
  • 实时热门榜单,用户疯狂点赞刷新,数据一致性还能保障吗?

这些挑战看似不可战胜,但其实只需一把小小的“锁”就能搞定。而今天要介绍的是一种革命性的实现方式——用 Redisson 自定义注解实现分布式锁,让复杂的分布式锁逻辑一秒化繁为简!

1.1 为什么是分布式锁?

分布式锁的诞生,就是为了解决多线程、多服务实例同时争夺共享资源的问题。没有它,你的系统可能会面临:

  • 库存超卖:用户买到不存在的商品,投诉接到爆炸!
  • 数据不一致:多个服务同时写入数据,结果一片混乱!
  • 资源争夺:高并发场景下,系统直接崩溃,用户流失殆尽!

1.2 为什么选择 Redisson?

Redisson 是 Redis 的超强武器,它提供了简单易用的 API 和强大的分布式工具,分布式锁更是它的杀手锏!Redisson 让你告别繁琐的 Redis 脚本操作,轻松实现:

  • 可重入锁:同一线程可以多次加锁,不会死锁!
  • 公平锁:让资源争夺变得有序,不再被流量大户压垮!
  • 读写锁:读写分离,性能暴增!
  • 信号量锁:轻松控制并发量,拒绝服务过载!

1.3 为什么要自定义注解?

用 Redisson 自定义注解,你可以这样玩:

  • 再也不用手写复杂的加锁、释放逻辑,只需一句注解就搞定:
@@RedissonLock(key = "order:lock")
public void placeOrder() {
    // 秒杀下单逻辑
}
  • 清晰优雅,代码整洁如诗;稳定高效,性能拉满!
  • 秒杀、抢单、排行榜……任何并发场景一招制敌!

1.4 准备好颠覆认知了吗?

Redisson 自定义分布式锁,将是你系统架构的必备利器。从此,高并发场景不再是噩梦,而是你的舞台!

不想再被“并发地狱”支配?探索分布式锁的秘密吧!

二、赛前准备

2.1 引入依赖

我这里引入了一些依赖,以便更好的实现自定义注解实现分布式锁

  1. redisson 3.21.0
  2. spring-boot-starter-aop 2.6.13
  3. lombok 2.6.13
  4. hutool 5.7.10
        <!-- Redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.21.0</version>
        </dependency>
        <!-- Spring Boot AOP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
          <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.10</version>
        </dependency>

2.2 添加配置

redisson配置类,自定义redis地址和使用的database

/**
 * @author yangp
 */
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://127.0.0.1:6379")
                .setDatabase(1);
        return Redisson.create(config);
    }
}

三、自定义枚举(定义锁类型)

2.1 标准锁介绍

标准锁(也称为非公平锁)是最常见的锁类型,它不保证线程获得锁的顺序。任何等待的线程都可以在锁释放时争夺锁,可能会发生"锁抢占"现象,导致某些线程长时间得不到锁。

  • 特性:不保证线程获取锁的顺序,先来先得、适用于大多数需要并发访问资源的场景。
  • 使用场景:适用于对性能要求较高且不太关注公平性的场景。

Redisson 中的实现RLock lock = redisson.getLock("myLock");

2.2 公平锁介绍

公平锁是一种确保锁的请求顺序与请求的顺序相同的锁。也就是说,先请求锁的线程先获得锁,避免了线程饥饿的问题。在高并发的情况下,公平锁能够保证线程按照请求的顺序获得锁,从而避免了资源的抢占和不公平竞争。

  • 特性:保证锁的公平性、先请求锁的线程先获得锁。
  • 使用场景:当多个线程需要公平地访问共享资源时,使用公平锁可以避免线程饥饿。

Redisson 中的实现RLock fairLock = redisson.getFairLock("myLock");

2.3 读锁介绍

读锁是属于读写锁(ReadWriteLock)的一部分,它允许多个线程同时获取读锁。当一个线程持有读锁时,其他线程也可以同时获取读锁,从而实现并发读操作。但是,写锁会阻塞所有的读锁和写锁。

  • 特性:多个线程可以并发地获得读锁、只有当没有线程持有写锁时,才能获取读锁。
  • 使用场景:适用于读多写少的场景,通过允许多个线程同时读取共享资源,提高性能。

Redisson 中的实现

  • RReadWriteLock rwLock = redisson.getReadWriteLock("rwLock");
  • RLock readLock = rwLock.readLock();

2.4 写锁介绍

写锁是属于读写锁(ReadWriteLock)的一部分。当一个线程持有写锁时,其他线程(无论是读锁还是写锁)都无法访问该资源,写锁是排它的

  • 特性:只有持有写锁的线程才能修改资源、写锁是排他性的,其他任何线程都不能同时持有写锁或读锁。
  • 使用场景:适用于写操作较少但需要保证数据一致性的场景,避免并发修改问题。

Redisson 中的实现

  • RReadWriteLock rwLock = redisson.getReadWriteLock("rwLock");
  • RLock writeLock = rwLock.writeLock();

2.5 枚举实现

这里的枚举使用到策略模式,好好品,每一个枚举实现类对应一种锁,枚举是天然的策略模式。

/**
 * 锁类型
 * @Author yangp
 */
@Getter
@AllArgsConstructor
public enum LockType implements IDictEnum {

    /**
     * 标准锁:不保证公平性
     */
    STANDARD(1, "标准锁", lockKey -> SpringUtil.getBean(RedissonClient.class).getFairLock(lockKey)),
    /**
     * 公平锁:保证公平性
     */
    FAIR(2, "公平锁", lockKey -> SpringUtil.getBean(RedissonClient.class).getReadWriteLock(lockKey).readLock()),
    /**
     * 读锁:保证读操作,不保证写操作
     */
    READ(3, "读锁", lockKey -> SpringUtil.getBean(RedissonClient.class).getReadWriteLock(lockKey).writeLock()),
    /**
     * 写锁:保证写操作,不保证读操作
     */
    WRITE(4, "写锁", lockKey -> SpringUtil.getBean(RedissonClient.class).getLock(lockKey));

    private final Integer type;
    private final String desc;

    /**
     * 锁类型
     */
    private final Function<String, RLock> lockTypeFun;

    /**
     * 获取锁类型
     *
     * @param lockType 锁类型
     * @return 锁类型
     */
    public RLock getLockType(String lockType) {
        return lockTypeFun.apply(lockType);
    }
}

四、自定义回调

4.1 回调接口

在下面的注解中提供一个一个回调接口,当出现方法超时、方法执行异常,提供一个兜底的方案。

/**
 * @Author: yangp
 * @Date: 2024/12/4 下午3:43
 */
public interface LockCallback {
    /**
     * 当无法获取锁时调用(方法超时)
     *
     * @param lockKey 锁的 Key
     */
    void onLockFailure(String lockKey);

    /**
     * 当执行方法抛出异常时调用
     *
     * @param lockKey 锁的 Key
     * @param ex      异常信息
     */
    void onException(String lockKey, Throwable ex);
}

4.2 默认回调

目前提供的默认回调,只是打印相关日志信息,可以根据具体的业务,实现默认回调,保证线程即使没有拿到锁,也会有记录,或者记录存入数据库,提供一个兜底的实现。

/**
 * @Author: yangp
 * @Date: 2024/12/4 下午3:50
 */
public class DefaultLockCallback implements LockCallback {

    private static final Logger logger = LoggerFactory.getLogger(DefaultLockCallback.class);

    /**
     * 当无法获取锁时调用(方法超时)
     *
     * @param lockKey 锁的 Key
     */
    @Override
    public void onLockFailure(String lockKey) {
        logger.error("[默认分布式锁回调][无法获取锁],获取锁失败,lockKey:{}", lockKey);
    }

    /**
     * 当执行方法抛出异常时调用
     *
     * @param lockKey 锁的 Key
     * @param ex      异常信息
     */
    @Override
    public void onException(String lockKey, Throwable ex) {
        logger.error("[默认分布式锁回调][执行方法抛出异常],执行方法抛出异常,lockKey:{}", lockKey, ex);
    }
}

五、自定义注解

注解参数介绍:

  1. key:锁的 key 支持静态和 SpEL 表达式的 Key,比如:@RedissonLock(key = "#account.userPkId:#account.money"),当然这里money不应该作为key,key应该保证唯一性和不变性。
  2. leaseTime:锁的持有时间(秒),也就是redis中数据的过期时间。
  3. waitForLock:是否等待锁,是等待锁则持续等待waitTime秒,不是等待锁,获取不到锁就返回失败。
/**
 * Redisson 分布式锁注解
 * @Author yangp
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLock {
    /**
     * 锁的 key 支持静态和 SpEL 表达式的 Key
     *
     * @return key
     */
    String spelKey();

    /**
     * 锁的持有时间(秒)
     *
     * @return time
     */
    long leaseTime() default 60;

    /**
     * 是否等待锁
     *
     * @return boolean
     */
    boolean waitForLock() default true;

    /**
     * 等待锁的时间(秒)
     *
     * @return time
     */
    long waitTime() default 5;

    /**
     * 锁回调类
     *
     * @return lockCallback
     */
    Class<? extends LockCallback> lockCallback() default DefaultLockCallback.class;

    /**
     * 锁类型
     *
     * @return lockType
     */
    LockType lockType() default LockType.STANDARD;
}

六、分布式锁切面

分布式锁 AOP 切面类,基于 Redisson 实现。主要用于在方法执行前后处理分布式锁的获取和释放逻辑。

/**
 * 分布式锁 AOP 切面类,基于 Redisson 实现。
 * 主要用于在方法执行前后处理分布式锁的获取和释放逻辑。
 *
 * @Author yangp
 */
@Aspect
@Component
@Slf4j
public class RedissonLockAspect {

    /**
     * Spring Expression Language (SpEL) 表达式解析器,用于解析注解中的动态表达式。
     */
    private static final ExpressionParser PARSER = new SpelExpressionParser();

    /**
     * 用于获取方法参数的名称
     */
    private static final DefaultParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();

    /**
     * 解析注解中的 SpEL 表达式,生成锁的键值。
     *
     * @param sp     注解中定义的 SpEL 表达式
     * @param method 当前方法对象
     * @param args   方法参数
     * @return 解析后的锁键
     */
    public static String parseSp(String sp, Method method, Object[] args) {
        // 创建 SpEL 表达式的上下文
        EvaluationContext context = new StandardEvaluationContext();
        String[] paramNames = NAME_DISCOVERER.getParameterNames(method);

        if (paramNames != null) {
            for (int i = 0; i < paramNames.length; i++) {
                // 将方法参数绑定到上下文中,参数名作为变量名
                context.setVariable(paramNames[i], args[i]);
            }
        }

        // 解析 SpEL 表达式,获取结果
        StringBuilder stringBuffer = new StringBuilder();
        String key;
        String[] split = sp.split(":");
        try {
            for (String s : split) {
                stringBuffer.append(PARSER.parseExpression(s).getValue(context, String.class));
                stringBuffer.append(":");
            }
            stringBuffer.deleteCharAt(stringBuffer.length() - 1);
            key = stringBuffer.toString();
        } catch (Exception e) {
            log.error("解析 SpEL 表达式失败将使用默认值字符作为Key,SpEL: {}, 异常信息: {}", sp, e.getMessage(), e);
            key = sp;
        }

        // 使用方法名作为前缀,并返回最终锁 Key
        String lockKey = method.getName() + ":" + key;

        // 对锁 Key 进行 MD5 压缩,避免长度过长
        return lockKey.length() > 100 ? DigestUtils.md5DigestAsHex(lockKey.getBytes(StandardCharsets.UTF_8)) : lockKey;
    }

    /**
     * AOP 切面方法,处理分布式锁的逻辑。
     *
     * @param joinPoint    切入点信息
     * @param redissonLock 分布式锁注解
     * @return 方法执行结果
     */
    @Around("@annotation(redissonLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        // 解析锁键
        String lockKey = parseSp(redissonLock.spelKey(), method, joinPoint.getArgs());
        // 获取 Redisson 的锁对象
        RLock lock = redissonLock.lockType().getLockType(lockKey);
        boolean isLocked = false;

        try {
            // 根据配置尝试获取锁
            if (redissonLock.waitForLock()) {
                log.info("[分布式锁][等待锁]等待获取分布式锁,key: {}", lockKey);
                isLocked = lock.tryLock(redissonLock.waitTime(), redissonLock.leaseTime(), TimeUnit.SECONDS);
            } else {
                log.info("[分布式锁][非等待锁]尝试获取分布式锁,key: {}", lockKey);
                isLocked = lock.tryLock(0, redissonLock.leaseTime(), TimeUnit.SECONDS);
            }

            // 如果未能获取锁,执行回调并抛出异常
            if (!isLocked) {
                log.warn("无法获取分布式锁,key: {}", lockKey);
                invokeCallback(redissonLock.lockCallback(), lockKey, null);
                throw new IllegalStateException("无法获取分布式锁,key: " + lockKey);
            }

            log.info("成功获取分布式锁,key: {}, 类名: {}, 方法名: {}, 线程: {}", lockKey, method.getDeclaringClass().getName(), method.getName(), Thread.currentThread().getId());

            // 执行目标方法
            return joinPoint.proceed();
        } catch (Throwable ex) {
            // 处理目标方法抛出的异常
            log.error("方法执行异常,key: {}, 异常信息: {}", lockKey, ex.getMessage(), ex);
            invokeCallback(redissonLock.lockCallback(), lockKey, ex);
            throw new IllegalStateException("方法执行异常,key: " + lockKey, ex);
        } finally {
            // 释放锁
            if (isLocked) {
                try {
                    // 确保当前线程持有锁
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                        log.info("成功释放分布式锁,key: {}, 线程: {}", lockKey, Thread.currentThread().getId());
                    }
                } catch (Exception e) {
                    log.error("释放锁时发生异常,key: {}, 线程: {}, 异常信息: {}", lockKey, Thread.currentThread().getId(), e.getMessage(), e);
                }
            }
        }
    }

    /**
     * 执行回调逻辑。
     *
     * @param callbackClass 回调类
     * @param lockKey       锁键
     * @param ex            异常信息
     */
    private void invokeCallback(Class<? extends LockCallback> callbackClass, String lockKey, Throwable ex) {
        if (!ObjectUtils.isEmpty(callbackClass)) {
            try {
                LockCallback callbackInstance = callbackClass.getDeclaredConstructor().newInstance();
                if (ex == null) {
                    callbackInstance.onLockFailure(lockKey);
                } else {
                    callbackInstance.onException(lockKey, ex);
                }
            } catch (Exception e) {
                log.error("回调方法调用失败,无法实例化回调类: {}, 异常信息: {}", callbackClass.getName(), e.getMessage(), e);
            }
        }
    }
}

七、分布式锁测试

7.1 测试准备

7.1.1 账户实体类

账户实体类,主要有用户主键和金额两个字段

/**
 * @author yangp
 */
@Data
public class Account {
    /**
     * 主键
     */
    private String userPkId;
    /**
     * 金额
     */
    private BigDecimal money;
}

7.1.2 账户控制器

接口的入口,这里只是做模拟,应该使用统一的返回值的。

/**
 * @Author: yangp
 * @Date: 2024/12/4 下午4:57
 */
@RestController
@RequestMapping("/account")
@Slf4j
public class AccountController {

    @Resource
    private AccountServiceImpl accountService;

    @PostMapping("/update")
    public String update(@RequestBody Account account) {
        return accountService.updateAccount(account);
    }
}

7.1.3 账户服务实现类(场景模拟)

假设有两个线程同时尝试更新同一个账户的余额,逻辑是这样的:

  1. 线程 A 和线程 B 都从数据库中读取账户的余额。
  2. 线程 A 将余额 +1000 后保存。
  3. 线程 B 也在几乎相同的时间点,读取账户余额并将余额 +500 保存。
  4. 如果没有分布式锁,线程 A 和线程 B 会互相覆盖对方的更新,导致账户余额不正确。
public interface IAccountService {
    String updateAccount(Account account);
}

@Service
@Slf4j
public class AccountServiceImpl implements IAccountService {
    /**
     * 模拟数据库,存储账户信息
     */
    public static Map<String, BigDecimal> accountDatabase = new HashMap<>();

    public AccountServiceImpl() {
        // 初始化账户余额 用户ID 111111,初始余额0
        accountDatabase.put("111111", new BigDecimal("0"));
    }
    /**
     * 更新账户余额,模拟并发问题
     */
    @Override
   /* // 使用Redisson分布式锁,保证每次只有一个线程可以更新同一账户
    @RedissonLock(spelKey = "'账户' + #account.userPkId", waitTime = 2000)  */
    public String updateAccount(Account account) {
        try {
            // 模拟处理时间,假设在这段时间内可能有多个线程同时尝试更新同一个账户
            // 模拟延迟,保证模拟并发问题
            Thread.sleep(1000);

            // 获取账户余额
            BigDecimal balance = accountDatabase.get(account.getUserPkId());
            if (balance == null) {
                throw new RuntimeException("Account not found");
            }

            // 假设账户余额加1000
            BigDecimal newBalance =  account.getMoney().add(balance);
            // 更新账户余额
            accountDatabase.put(account.getUserPkId(), newBalance);

            log.info("Updated account: {}", accountDatabase.get(account.getUserPkId()));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "success";
    }
}

7.1.4 集群配置

在微服务开发场景下,服务大多是以集群方式部署的。在本地开发时 有时候会需要以集群的方式启动项目,同时启动多个实例来测试一些相关功能(例如分布式锁),此时不需要打包之后再仍到测试环境去启动多个实例,在IDE中就可以同时启动多个实例。

编辑配置

复制配置

快捷键 alt + e 添加环境变量,自定义端口号

--server.port=8099

7.2 不使用分布式锁

这里模拟了5个线程来跑,演示多个线程互相覆盖对方的更新,导致账户余额不正确:

@SpringBootTest(classes = SpringbootDemoApplication.class)
@Slf4j
class AccountServiceImplTest {
    @Autowired
    private IAccountService accountService;
    @Test
    void testUpdateAccountWithoutLock() throws InterruptedException {
        // 使用线程池模拟多线程环境
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        // 用于等待所有线程完成
        CountDownLatch latch = new CountDownLatch(5);
        // 模拟并发更新
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                try {
                    // 创建一个账户对象,模拟更新操作
                    Account account = new Account();
                    // 账户ID
                    account.setUserPkId("111111");
                    // 每个线程试图加1000
                    account.setMoney(new BigDecimal("1000"));
                    // 调用更新账户的方法
                    accountService.updateAccount(account);
                } finally {
                    latch.countDown(); // 线程完成,减少计数
                }
            });
        }
        // 等待所有线程完成
        latch.await();
        executorService.shutdown();
        // 输出最终账户余额
        System.out.println("Final account balance: " + AccountServiceImpl.accountDatabase.get("111111"));
        // 如果没有加锁,最终账户余额不是5000
        System.out.println("Expected account balance: 5000");
    }
}

可以看到最终的账户余额(2000) != 预期的账户余额(5000),出现了线程安全问题。

7.3 使用分布式锁

一个执行过程需要1秒,我们默认为等待锁,设置了等待时间为2秒,5个线程对余额分别 + 1000,最终账户余额预期为:5000。

  /**
     * 更新账户余额,模拟并发问题
     */
    @Override
    @RedissonLock(spelKey = "'账户' + #account.userPkId", waitTime = 2000)// 使用Redisson分布式锁,保证每次只有一个线程可以更新同一账户
    public String updateAccount(Account account) {
        try {
            // 模拟处理时间,假设在这段时间内可能有多个线程同时尝试更新同一个账户
            // 模拟延迟,保证模拟并发问题
            Thread.sleep(1000);

            // 获取账户余额
            BigDecimal balance = accountDatabase.get(account.getUserPkId());
            if (balance == null) {
                throw new RuntimeException("Account not found");
            }

            // 假设账户余额加1000
            BigDecimal newBalance =  account.getMoney().add(balance);
            // 更新账户余额
            accountDatabase.put(account.getUserPkId(), newBalance);

            log.info("Updated account: {}", accountDatabase.get(account.getUserPkId()));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "success";
    }

以看到最终的账户余额(5000) == 预期的账户余额(5000)

7.3 标准锁测试

7.3.1 @RedissonLock(key = "#account.userPkId"):等待锁

  1. key:包名.类名.方法名.hash(参数)
  2. leaseTime:锁持有时间60秒
  3. waitForLock:为等待锁
  4. waitTime:等待时间为5秒
  5. 锁类型:标准锁,不保证公平性

服务器:8099 成功获取到锁,并执行中

服务器:8899 没有获取到锁,等待5秒,5秒后执行回调,并抛出异常

7.3.2 @RedissonLock(key = "#account.userPkId",waitForLock = false):非等待锁

  1. key:包名.类名.方法名.hash(参数)
  2. leaseTime:锁持有时间60秒
  3. waitForLock:非等待锁
  4. 锁类型:标准锁,不保证公平性

8899 成功获取到锁,并执行中

8099没有获取到锁,直接执行回调,并抛出异常

八、4大锁类型使用场景总结

最后给大家献上每种锁类型的使用场景,方便各位大佬食用。

锁类型使用场景
公平锁金融、银行系统的资金操作,确保用户的请求按顺序处理,防止先到的请求被长时间阻塞。
标准锁任务调度系统中,如果任务处理顺序不重要,使用标准锁可以提高吞吐量。
读锁读多写少的场景 、 数据读取不频繁改变时
写锁写操作独占资源的场景、 数据修改、更新操作