基于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);
@Autowired
private LockMapper lockMapper;
@Autowired
private 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 {
@Autowired
private DistributedLockService lockService;
@Autowired
private 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 {
@Autowired
private 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,确保锁只能被持有者释放