java每日精进 5.27【分布式锁】

发布于:2025-05-28 ⋅ 阅读:(25) ⋅ 点赞:(0)

项目利用 Redis 实现分布式锁,提供两种使用方式:

编程式锁(通过 Redisson 显式加锁/解锁)和

声明式锁(通过 @Lock4j 注解自动管理锁)

以下以支付通知模块(PayNotify)为例,详细解析两种方式的实现过程。

1. 编程式锁(基于 Redisson)

编程式锁通过 Redisson 框架提供灵活的分布式锁操作,适合需要精细控制锁逻辑的场景。 PayNotify 模块使用编程式锁确保支付通知任务的并发安全。

实现步骤
步骤 1:引入 Redisson 依赖

在模块的 pom.xml 中添加 Redisson 依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.25.1</version> <!-- 建议使用最新版本 -->
</dependency>
  • 解释
    • redisson-spring-boot-starter 提供 Redisson 客户端,支持多种分布式锁类型(如普通锁、红锁、读写锁)。
    • 已在 Redis 缓存模块配置好 Redisson 和 Spring Data Redis,无需额外配置。
步骤 2:定义 Redis 锁键

在 RedisKeyConstants 接口中,定义支付通知任务的分布式锁键:

/**
 * System Redis Key 枚举类
 */
public interface RedisKeyConstants {

    /**
     * 指定部门的所有子部门编号数组的缓存
     * <p>
     * KEY 格式:dept_children_ids:{id}
     * VALUE 数据类型:String 子部门编号集合
     */
    String DEPT_CHILDREN_ID_LIST = "dept_children_ids";

    /**
     * 角色的缓存
     * <p>
     * KEY 格式:role:{id}
     * VALUE 数据类型:String 角色信息
     */
    String ROLE = "role";

    /**
     * 用户拥有的角色编号的缓存
     * <p>
     * KEY 格式:user_role_ids:{userId}
     * VALUE 数据类型:String 角色编号集合
     */
    String USER_ROLE_ID_LIST = "user_role_ids";

    /**
     * 拥有指定菜单的角色编号的缓存
     * <p>
     * KEY 格式:menu_role_ids:{menuId}
     * VALUE 数据类型:String 角色编号集合
     */
    String MENU_ROLE_ID_LIST = "menu_role_ids";

    /**
     * 拥有权限对应的菜单编号数组的缓存
     * <p>
     * KEY 格式:permission_menu_ids:{permission}
     * VALUE 数据类型:String 菜单编号数组
     */
    String PERMISSION_MENU_ID_LIST = "permission_menu_ids";

    /**
     * OAuth2 客户端的缓存
     * <p>
     * KEY 格式:oauth_client:{id}
     * VALUE 数据类型:String 客户端信息
     */
    String OAUTH_CLIENT = "oauth_client";

    /**
     * 访问令牌的缓存
     * <p>
     * KEY 格式:oauth2_access_token:{token}
     * VALUE 数据类型:String 访问令牌信息 {@link OAuth2AccessTokenDO}
     * <p>
     * 由于动态过期时间,使用 RedisTemplate 操作
     */
    String OAUTH2_ACCESS_TOKEN = "oauth2_access_token:%s";

    /**
     * 站内信模版的缓存
     * <p>
     * KEY 格式:notify_template:{code}
     * VALUE 数据格式:String 模版信息
     */
    String NOTIFY_TEMPLATE = "notify_template";

    /**
     * 邮件账号的缓存
     * <p>
     * KEY 格式:mail_account:{id}
     * VALUE 数据格式:String 账号信息
     */
    String MAIL_ACCOUNT = "mail_account";

    /**
     * 邮件模版的缓存
     * <p>
     * KEY 格式:mail_template:{code}
     * VALUE 数据格式:String 模版信息
     */
    String MAIL_TEMPLATE = "mail_template";

    /**
     * 短信模版的缓存
     * <p>
     * KEY 格式:sms_template:{id}
     * VALUE 数据格式:String 模版信息
     */
    String SMS_TEMPLATE = "sms_template";

    /**
     * 小程序订阅模版的缓存
     *
     * KEY 格式:wxa_subscribe_template:{userType}
     * VALUE 数据格式 String, 模版信息
     */
    String WXA_SUBSCRIBE_TEMPLATE = "wxa_subscribe_template";

}
  • 解释
    • 键格式为 pay_notify:lock:{id},如 pay_notify:lock:123,确保每个任务有唯一锁。
    • 使用模板字符串,便于动态生成。
步骤 3:实现锁操作类

创建 PayNotifyLockRedisDAO 类,使用 Redisson 实现加锁和解锁:

@Repository
public class PayNotifyLockRedisDAO {

    @Resource
    private RedissonClient redissonClient;

    public void lock(long id, long timeoutMillis, Runnable action) {
        String lockKey = formatKey(id);
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 加锁
            lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
            // 执行逻辑
            action.run();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    private static String formatKey(Long id) {
        return String.format(RedisKeyConstants.PAY_NOTIFY_LOCK, id);
    }
}

解释

  • 依赖注入:RedissonClient 由 Redisson Starter 提供。
  • 方法 lock
    1. 生成键:根据任务 ID 格式化锁键。
    2. 获取锁:通过 redissonClient.getLock(key) 获取锁对象,lock.lock(timeoutMillis, TimeUnit.MILLISECONDS) 设置加锁超时。
    3. 执行逻辑:在 try 块中执行传入的 Runnable 逻辑。
    4. 释放锁:在 finally 块中调用 lock.unlock(),确保锁释放。
  • 安全性:try-finally 结构保证锁一定释放,防止死锁。
步骤 4:应用锁

在 PayNotifyServiceImpl 中使用 PayNotifyLockRedisDAO 加锁:

@Service
public class PayNotifyServiceImpl implements PayNotifyService {

    public static final long NOTIFY_TIMEOUT_MILLIS = 120 * 1000; // 120秒

    @Resource
    private PayNotifyLockRedisDAO payNotifyLockRedisDAO;
    @Resource
    private PayNotifyTaskMapper payNotifyTaskMapper;

    @Override
    public void executeNotifySync(PayNotifyTaskDO task) {
        payNotifyLockRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> {
            // 校验任务是否已过期
            PayNotifyTaskDO dbTask = payNotifyTaskMapper.selectById(task.getId());
            if (DateUtils.afterNow(dbTask.getNextNotifyTime())) {
                log.info("[executeNotify][任务({}) 忽略,未到通知时间]", dbTask.getId());
                return;
            }
            // 执行通知逻辑
            executeNotify(dbTask);
        });
    }

    private void executeNotify(PayNotifyTaskDO task) {
        // 模拟通知逻辑
        log.info("[executeNotify][执行任务 {}]", task.getId());
    }
}
  • 解释
    • 注入:注入 PayNotifyLockRedisDAO 用于锁操作。
    • 加锁:调用 lock 方法,传入任务 ID、超时时间(120秒)和业务逻辑(Runnable)。
    • 校验:加锁后再次查询任务状态,防止并发重复执行。
    • 执行:调用 executeNotify 完成通知。
    • 优势:编程式锁显式控制锁的范围和释放时机,适合复杂逻辑。
步骤 5:为什么选择 Redisson?
  • 多类型锁:支持普通锁、红锁、读写锁等,满足不同场景。
  • 高可靠性:内置看门狗机制,自动延长锁超时时间。
  • 易用性:API 直观,集成 Spring 简单。

2. 声明式锁(基于 Lock4j)

声明式锁通过 Lock4j 的 @Lock4j 注解提供简洁的分布式锁支持,适合快速开发。Yudao 默认未启用 Lock4j,需手动引入。

实现步骤
步骤 1:引入 Lock4j 依赖

在 pom.xml 中添加 Lock4j 依赖:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
    <version>2.2.4</version>
</dependency>
  • 解释
    • lock4j-redisson-spring-boot-starter 整合 Lock4j 和 Redisson,提供 Redis 分布式锁。
    • 默认 optional=true,需移除以启用。
步骤 2:配置 Lock4j

在 application-local.yml 中配置 Lock4j 参数:

lock4j:
  acquire-timeout: 3000 # 获取锁超时时间(毫秒)
  expire: 30000 # 锁过期时间(毫秒)

  • 解释
    • acquire-timeout:尝试获取锁的最长时间,超时后抛出异常。
    • expire:锁的自动过期时间,防止死锁。
    • 默认值适合大多数场景,可根据业务调整。
步骤 3:应用 @Lock4j 注解

在服务方法上添加 @Lock4j 注解:

@Service
public class DemoService {

    @Lock4j
    public void simple() {
        // 模拟业务逻辑
        log.info("[simple][执行简单逻辑]");
    }

    @Lock4j(keys = {"#user.id", "#user.name"}, expire = 60000, acquireTimeout = 1000)
    public User customMethod(User user) {
        log.info("[customMethod][处理用户 {}]", user);
        return user;
    }
}

@Data
class User {
    private Long id;
    private String name;
}
  • 解释
    • 简单锁:simple 方法使用默认配置,锁键基于方法签名。
    • 自定义锁:customMethod 使用 SpEL 表达式(#user.id, #user.name)生成锁键,设置 60秒过期和 1秒获取超时。
    • 机制:Lock4j 自动在方法执行前加锁,执行后解锁,基于 Redis 实现。
步骤 4:Lock4j 的优势
  • 简洁:注解式开发,减少样板代码。
  • 灵活:支持 SpEL 表达式自定义锁键。
  • 多后端:支持 Redis、ZooKeeper 等,Redisson 仅为一种实现。