基于mysql数据库实现分布式锁
目录
前言(背景-思路):
1.新增锁锁表
2.具体代码实现
2.1 工具方法
2.2 mapper实现
2.3 测试小列子
2.4 极端情况服务挂了,数据存锁一直得不到释放
前言(背景-思路):
我们组redis翻车了,给客户带来问题。虽然紧急出包得到了解决,但是大领导一句话,直接禁用了。下面是我基于mysql(oracle)数据库实现的分布式锁:
主题思路就是,借助数据库的行锁,然后拿到锁则继续执行,拿到锁并启动ScheduledExecutorService (定时周期执行任务-每隔锁过期时间/3续期一次,类似看门狗机制),拿不到锁则自旋等待。
废话不多说上代码-拿去即可使用:
实现背景:springboot、mybatis、mybatis-plus
1.新增锁锁表
CREATE TABLE `cache_lock_info` (
`lock_name` varchar(255) NOT NULL,
`lock_value` varchar(255) NOT NULL,
`expire_date_time` datetime NOT NULL,
`update_date` date NOT NULL,
`update_time` time NOT NULL,
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
2.具体代码实现
2.1 工具方法
package onlyqi.dayday01lock.lock;
import onlyqi.dayday01lock.lock.mapper.LockInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service
public class DistributedLockService {
@Autowired
private LockInfoMapper lockInfoMapper;
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
/**
* 尝试获取锁并启动续期任务
*
* @param lockName 锁名称
* @param lockValue 锁值(唯一标识)
* @param expireSec 锁过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLockWithRenewal(String lockName, String lockValue, int expireSec) {
Date now = new Date();
Date expireDateTime = new Date(now.getTime() + expireSec * 1000L);
try {
lockInfoMapper.insertLock(lockName, lockValue, expireDateTime, now, now);
// 检查线程池是否已关闭,如果已关闭则重新初始化
if (scheduler.isShutdown()) {
scheduler = Executors.newScheduledThreadPool(1);
}
// 启动续期任务
scheduler.scheduleAtFixedRate(() -> {
if (!renewLock(lockName, lockValue, expireSec)) {
// 续期失败,停止任务
scheduler.shutdown();
}
}, expireSec / 3, expireSec / 3, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
// ===========防止服务挂了,数据存在过期锁一直得不到释放====================//
// 锁已过期,删除旧记录并重新获取锁=======然后返回false,下次自旋获取====此方式不太推荐,推荐服务挂了,再次部署的时候删除数据库所有过期的key。此方法仅做参考//
lockInfoMapper.deleteLockExpire(lockName,expireDateTime);
// 主键冲突,锁已被其他线程持有
return false;
}
}
/**
* 阻塞自旋获取锁
*
* @param lockName 锁名称
* @param lockValue 锁值(唯一标识)
* @param expireSec 锁过期时间(秒)
* @param timeoutSec 自旋超时时间(秒)
* @return 是否获取成功
*/
public boolean spinLock(String lockName, String lockValue, int expireSec, int timeoutSec) {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < timeoutSec * 1000L) {
if (tryLockWithRenewal(lockName, lockValue, expireSec)) {
// 获取锁成功
return true;
}
try {
Thread.sleep(100); // 自旋间隔
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
// 超时未获取锁
return false;
}
/**
* 续期锁
*
* @param lockName 锁名称
* @param lockValue 锁值(唯一标识)
* @param expireSec 续期时间(秒)
* @return 是否续期成功
*/
public boolean renewLock(String lockName, String lockValue, int expireSec) {
Date now = new Date();
Date expireDateTime = new Date(now.getTime() + expireSec * 1000L);
// LockInfo lockInfo = lockInfoMapper.selectById(lockName);
// lockInfo.setExpireDateTime(expireDateTime);
// int rowsUpdated = lockInfoMapper.updateById(lockInfo);
int rowsUpdated = lockInfoMapper.renewLock(lockName, lockValue, expireDateTime, now, now);
return rowsUpdated > 0; // 更新成功表示续期成功
}
/**
* 释放锁并停止续期任务
*
* @param lockName 锁名称
* @param lockValue 锁值(唯一标识)
* @return 是否释放成功
*/
public boolean releaseLockWithRenewal(String lockName, String lockValue) {
scheduler.shutdown(); // 停止续期任务
return releaseLock(lockName, lockValue);
}
/**
* 释放锁
*
* @param lockName 锁名称
* @param lockValue 锁值(唯一标识)
* @return 是否释放成功
*/
public boolean releaseLock(String lockName, String lockValue) {
int rowsDeleted = lockInfoMapper.deleteLock(lockName, lockValue);
return rowsDeleted > 0; // 删除成功表示释放成功
}
/**
* 销毁时关闭线程池
*/
@PreDestroy
public void destroy() {
if (scheduler != null) {
scheduler.shutdown();
}
}
}
2.2 mapper实现
package onlyqi.dayday01lock.lock.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import onlyqi.dayday01lock.lock.LockInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
@Mapper
public interface LockInfoMapper extends BaseMapper<LockInfo> {
int insertLock(@Param("lockName") String lockName, @Param("lockValue") String lockValue,
@Param("expireDateTime") Date expireDateTime, @Param("updateDate") Date updateDate,
@Param("updateTime") Date updateTime);
int renewLock(@Param("lockName") String lockName, @Param("lockValue") String lockValue,
@Param("expireDateTime") Date expireDateTime, @Param("updateDate") Date updateDate,
@Param("updateTime") Date updateTime);
int deleteLock(@Param("lockName") String lockName, @Param("lockValue") String lockValue);
int deleteLockExpire(@Param("lockName") String lockName, @Param("expireDateTime") Date expireDateTime);
}
2.3 测试小列子
package onlyqi.dayday01lock.lock;
import onlyqi.dayday01lock.lock.mapper.LockInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DistributedLockExample {
@Autowired
private DistributedLockService distributedLockService;
@Autowired
private LockInfoMapper lockInfoMapper;
public void lockTest(String operatorNo) {
String lockName = "my_lock";
String lockValue = Thread.currentThread().getName();
int expireSec = 30; // 锁过期时间为 30 秒
try {
// 尝试获取锁并启动续期任务
if (distributedLockService.spinLock(lockName, lockValue, expireSec,60)) {
System.out.println("Lock acquired successfully!======operatorNo:"+operatorNo+"========在执行");
// 模拟业务逻辑-模拟业务逻辑执行3 秒
Thread.sleep(3000);
System.out.println("Business logic completed!==="+operatorNo+"===执行结束=====");
} else {
System.out.println("Failed to acquire lock!"+operatorNo);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁并停止续期任务
if (distributedLockService.releaseLockWithRenewal(lockName, lockValue)) {
System.out.println("Lock released successfully!");
}
}
}
}
2.4 极端情况服务挂了,数据存锁一直得不到释放
实现一:每次获取不到锁的时候,增加删除数据库过期的锁的动作
实现二:启动项目的时候,清除数据库所有过期的锁(推荐)
@Component
public class StartupTask {
@PostConstruct
public void init() {
// 在应用启动时执行的逻辑
Date now = new Date();
Date nowDateTime = new Date(now.getTime());
deleteExpirekdKey(nowDateTime )
}
}
下一篇写基于mysql数据库实现缓存
完整demo代码已上传github:https://github.com/qi-only/daydayuo-go