使用MySQL實現分散式鎖
阿新 • • 發佈:2022-04-14
分散式鎖我們經常使用,在我們專案多節點部署或者微服務專案中經常使用;使用的方式有很多種,最常見的有Redis,zookeeper,資料庫等;對應專案本身併發不高而言,zookeeper和redis都需要我們單獨部署甚至搭建叢集去提高可用性。這對於服務資源本身不夠的機器來說更是雪上加霜,不過mysql這種作為一個儲存功能應用,我們離不開它,所以用它來實現分散式鎖,不需要額外的去維護一個應用,實現起來也比較簡單。
優點:簡單高效可靠
缺點:併發效能較低,功能相對來說比較單一
本次演示使用的ORM框架為 MybatisPlus+SpringBoot
1.建立資料庫表
這裡我的主鍵並未使用自增,因為解鎖時會利用主鍵去做唯一判斷,這樣採用了雪花演算法實現主鍵;
CREATE TABLE `lock_info` ( `id` bigint(20) unsigned NOT NULL, `expiration_time` datetime DEFAULT NULL COMMENT '過期時間', `status` tinyint(1) DEFAULT NULL COMMENT '鎖狀態,0,未鎖,1,已經上鎖', `tag` varchar(255) DEFAULT NULL COMMENT '鎖的標識,如專案id', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, KEY `uni_tag` (`tag`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='資料庫分散式鎖表';
2.程式碼邏輯
實體物件,id採用雪花演算法
@Data @TableName(value = "lock_info") public class LockInfo implements Serializable { private static final long serialVersionUID = 1L; public static final Integer LOCKED_STATUS = 1; public static final Integer UNLOCKED_STATUS = 0; /** * 最大超時時間,超過將刪除 */ public static final Integer MAX_TIMEOUT_SECONDS = 120; @TableId(value = "id", type = IdType.ASSIGN_ID) private Long id; /** * 鎖過期時間 */ private Date expirationTime; /** * 鎖狀態,0,未鎖,1,已經上鎖 */ private Integer status = LOCKED_STATUS; /** * 鎖的標識,如專案id */ private String tag; private Date createTime; private Date updateTime; }
鎖的資料檢視物件
public class LockVo implements Serializable {
/**
* 鎖的id
*/
private Long lockId;
/**
* 鎖過期時間
*/
private Date expirationTime;
/**
* 鎖的標識,如專案id
*/
private final String tag;
public LockVo(String tag) {
this.tag = tag;
}
public void setLockId(Long lockId) {
this.lockId = lockId;
}
public void setExpirationTime(Date expirationTime) {
this.expirationTime = expirationTime;
}
public Long getLockId() {
return lockId;
}
public Date getExpirationTime() {
return expirationTime;
}
public String getTag() {
return tag;
}
@Override
public String toString() {
return "LockVo{" +
"lockId=" + lockId +
", expirationTime=" + expirationTime +
", tag='" + tag + '\'' +
'}';
}
}
mapper物件
public interface LockInfoMapper extends BaseMapper<LockInfo> {
}
service介面
public interface ILockInfoService extends IService<LockInfo> {
/**
* 根據鎖標識獲取鎖資訊
*
* @param tag 鎖標識
* @return com.chinaunicom.deliver.api.model.eo.LockInfo
*/
LockInfo findByTag(String tag);
/**
* 嘗試獲取鎖
*
* @param lockVo 鎖的資料資訊
* @param expiredSeconds 鎖的過期時間(單位:秒),預設10s
* @return boolean
*/
boolean tryLock(LockVo lockVo, Integer expiredSeconds);
/**
* 嘗試獲取鎖,預設鎖定10秒
*
* @param lockVo 鎖的資料資訊
* @return boolean
*/
boolean tryLock(LockVo lockVo);
/**
* 釋放鎖
*
* @param lockVo 鎖的資料物件
*/
void unlock(LockVo lockVo);
}
service實現類
@Service
public class LockInfoServiceImpl extends ServiceImpl<LockInfoMapper, LockInfo> implements ILockInfoService {
private static final Integer DEFAULT_EXPIRED_SECONDS = 10;
@Autowired
private PlatformTransactionManager platformTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Override
public boolean tryLock(LockVo lockVo, Integer expiredSeconds) {
if (lockVo == null || StringUtils.isEmpty(lockVo.getTag())) {
throw new NullPointerException();
}
Date now = new Date();
LockInfo lock = findByTag(lockVo.getTag());
TransactionStatus transaction = null;
try {
if (Objects.isNull(lock)) {
transaction = platformTransactionManager.getTransaction(transactionDefinition);
lock = new LockInfo(lockVo.getTag(), this.getExpiredSeconds(new Date(), expiredSeconds));
this.save(lock);
platformTransactionManager.commit(transaction);
lockVo.setLockId(lock.getId());
return true;
} else {
Date expiredTime = lock.getExpirationTime();
if (expiredTime.before(now)) {
transaction = platformTransactionManager.getTransaction(transactionDefinition);
// 如果過期並且超過過期時間120秒之後,將刪除鎖資料
if (expiredTime.before(getExpiredSeconds(expiredTime, LockInfo.MAX_TIMEOUT_SECONDS))) {
this.removeById(lock.getId());
}
lock.setExpirationTime(this.getExpiredSeconds(now, expiredSeconds));
lock.setId(null);
this.save(lock);
platformTransactionManager.commit(transaction);
lockVo.setLockId(lock.getId());
return true;
}
}
} catch (Exception e) {
if (transaction != null) {
platformTransactionManager.rollback(transaction);
}
}
return false;
}
@Override
public boolean tryLock(LockVo lockVo) {
return this.tryLock(lockVo, DEFAULT_EXPIRED_SECONDS);
}
@Override
@Transactional(rollbackFor = Throwable.class)
public void unlock(LockVo lockVo) {
if (lockVo == null || StringUtils.isEmpty(lockVo.getTag()) || lockVo.getLockId() == null) {
throw new NullPointerException();
}
LockInfo info = getById(lockVo.getLockId());
if (info == null || !lockVo.getTag().equals(info.getTag())) {
return;
}
this.removeById(info.getId());
}
private Date getExpiredSeconds(Date date, Integer seconds) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.SECOND, seconds);
return calendar.getTime();
}
@Override
public LockInfo findByTag(String tag) {
return this.lambdaQuery().eq(LockInfo::getTag, tag)
.orderByDesc(LockInfo::getExpirationTime)
.last("limit 1").one();
}
}
1.注意事項
- 可以看到我加鎖的方法是有加上事務註解並且配置傳播規則為Propagation.NOT_SUPPORTED(以非事務的方式執行,如果當前存在事務,則掛起當前事務),這樣配置是為了使用手動事務,如果不加上該註解,SpringBoot會自動幫我們加入當前事務,這樣就沒辦法手動提交事務;這樣會導致在併發時,我們的加鎖事務在會和外部事務一起提交,在預設的隔離級別下面,其他執行緒的事務是沒辦法讀取未提交的事務,也就是說我們加的鎖沒有儲存進資料庫,其他執行緒一樣可以加鎖,這樣就導致加鎖失敗了
@Transactional(propagation = Propagation.NOT_SUPPORTED)
- 加鎖的時候會查詢當前鎖物件tag在表中過期時間最長的那個資料,避免鎖過期沒有釋放,一個tag對應多個值的問題。並且在解鎖的時候需要用主鍵id和tag對應唯一值,刪除了其他加了鎖的資料。
- 什麼時候會出現刪除其他鎖物件的資料:當一個操作執行的時間過長,獲取的鎖已經過期,此時其他同樣需要這個鎖的任務是能夠獲取鎖的,那麼此時表中相同的tag資料至少是2條以上,如果不使用主鍵id和tag做唯一標識,那麼在釋放鎖的時候就會把別的任務加的鎖一起刪除了,導致其他任務釋放鎖失敗!