基于MySQL的分布式锁实现(Spring Boot + MyBatis)
基于MySQL的分布式锁实现(Spring Boot + MyBatis)
实现原理
基于数据库的唯一索引特性实现分布式锁,通过插入唯一索引记录表示获取锁,删除记录表示释放锁。
1. 创建锁表
首先需要在MySQL中创建一个锁表,用于存储锁信息:
CREATE TABLE `distributed_lock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`lock_key` varchar(64) NOT NULL COMMENT '锁标识',`request_id` varchar(128) NOT NULL COMMENT '请求唯一标识',`expire_time` datetime NOT NULL COMMENT '过期时间',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_lock_key` (`lock_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';
2. 定义数据访问层
创建Lock实体类和Mapper接口:
// Lock.java
@Data
public class Lock {private Long id;private String lockKey;private String requestId;private LocalDateTime expireTime;private LocalDateTime createTime;
}// LockMapper.java
public interface LockMapper {/*** 获取锁(插入记录)*/int insertLock(Lock lock);/*** 释放锁(删除记录)*/int deleteLock(@Param("lockKey") String lockKey, @Param("requestId") String requestId);/*** 检查锁是否存在*/int checkLockExists(String lockKey);/*** 清除过期的锁*/int clearExpiredLocks();
}// LockMapper.xml
<mapper namespace="com.example.mapper.LockMapper"><insert id="insertLock">INSERT INTO distributed_lock (lock_key, request_id, expire_time)VALUES (#{lockKey}, #{requestId}, #{expireTime})</insert><delete id="deleteLock">DELETE FROM distributed_lock WHERE lock_key = #{lockKey} AND request_id = #{requestId}</delete><select id="checkLockExists" resultType="int">SELECT COUNT(1) FROM distributed_lock WHERE lock_key = #{lockKey}</select><delete id="clearExpiredLocks">DELETE FROM distributed_lock WHERE expire_time < NOW()</delete>
</mapper>
3. 实现分布式锁服务
创建分布式锁服务类,实现锁的获取和释放逻辑:
// DistributedLockService.java
@Service
public class DistributedLockService {private static final Logger logger = LoggerFactory.getLogger(DistributedLockService.class);@Autowiredprivate LockMapper lockMapper;@Autowiredprivate TransactionTemplate transactionTemplate;/*** 获取锁* @param lockKey 锁标识* @param expireSeconds 过期时间(秒)* @return 是否获取成功*/public boolean acquireLock(String lockKey, int expireSeconds) {String requestId = UUID.randomUUID().toString();Lock lock = new Lock();lock.setLockKey(lockKey);lock.setRequestId(requestId);lock.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));try {// 利用唯一索引特性,插入成功则获取锁成功int result = lockMapper.insertLock(lock);if (result > 0) {// 将requestId存入ThreadLocal,用于释放锁时验证ThreadLocalUtil.set("requestId", requestId);return true;}} catch (DuplicateKeyException e) {// 锁已被其他线程持有logger.debug("获取锁失败,锁已存在: {}", lockKey);} catch (Exception e) {logger.error("获取锁异常", e);}// 清除过期锁(可优化为定时任务)clearExpiredLocks();return false;}/*** 释放锁* @param lockKey 锁标识* @return 是否释放成功*/public boolean releaseLock(String lockKey) {String requestId = (String) ThreadLocalUtil.get("requestId");if (requestId == null) {logger.warn("未找到requestId,可能未获取锁或已释放锁");return false;}try {// 在事务中删除锁,确保原子性return transactionTemplate.execute(status -> {int result = lockMapper.deleteLock(lockKey, requestId);if (result > 0) {ThreadLocalUtil.remove("requestId");return true;}return false;});} catch (Exception e) {logger.error("释放锁异常", e);return false;}}/*** 清除过期的锁*/private void clearExpiredLocks() {try {lockMapper.clearExpiredLocks();} catch (Exception e) {logger.error("清除过期锁异常", e);}}
}
4. 使用分布式锁
在需要使用分布式锁的业务方法中调用锁服务:
// OrderService.java
@Service
public class OrderService {@Autowiredprivate DistributedLockService lockService;@Autowiredprivate StockService stockService;/*** 下单扣库存(使用分布式锁)*/public void placeOrder(String productId, int quantity) {String lockKey = "product_lock:" + productId;boolean lockAcquired = false;try {// 获取锁,设置超时时间为10秒lockAcquired = lockService.acquireLock(lockKey, 10);if (lockAcquired) {// 获取锁成功,执行扣库存操作stockService.reduceStock(productId, quantity);// 其他业务逻辑...} else {// 获取锁失败,处理重试或返回失败throw new BusinessException("系统繁忙,请稍后重试");}} finally {// 释放锁if (lockAcquired) {lockService.releaseLock(lockKey);}}}
}
5. 定时任务清理过期锁
为避免数据库中积累过多过期锁记录,添加定时任务定期清理:
// LockCleanupTask.java
@Component
public class LockCleanupTask {@Autowiredprivate LockMapper lockMapper;@Scheduled(fixedDelay = 60 * 1000) // 每分钟执行一次public void cleanupExpiredLocks() {try {int count = lockMapper.clearExpiredLocks();logger.info("清理过期锁完成,共清理: {}", count);} catch (Exception e) {logger.error("清理过期锁异常", e);}}
}
实现说明
- 获取锁:通过向数据库插入带有唯一索引的记录实现,插入成功则获取锁成功
- 释放锁:通过删除对应记录实现,需验证requestId确保安全性
- 锁超时:通过设置expire_time字段实现,配合定时任务清理过期锁
- 防误释放:使用ThreadLocal存储requestId,确保锁只能被持有者释放