java每日精进 5.27【分布式锁】
项目利用 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 {@Resourceprivate 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:
- 生成键:根据任务 ID 格式化锁键。
- 获取锁:通过 redissonClient.getLock(key) 获取锁对象,lock.lock(timeoutMillis, TimeUnit.MILLISECONDS) 设置加锁超时。
- 执行逻辑:在 try 块中执行传入的 Runnable 逻辑。
- 释放锁:在 finally 块中调用 lock.unlock(),确保锁释放。
- 安全性:try-finally 结构保证锁一定释放,防止死锁。
步骤 4:应用锁
在 PayNotifyServiceImpl 中使用 PayNotifyLockRedisDAO 加锁:
@Service
public class PayNotifyServiceImpl implements PayNotifyService {public static final long NOTIFY_TIMEOUT_MILLIS = 120 * 1000; // 120秒@Resourceprivate PayNotifyLockRedisDAO payNotifyLockRedisDAO;@Resourceprivate PayNotifyTaskMapper payNotifyTaskMapper;@Overridepublic 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 {@Lock4jpublic 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 仅为一种实现。