分布式鎖開發中經常使用,在項目多節點部署或者微服務項目中,JAVA提供的線程鎖已經不能滿足安全的需求,需要使用全局的分布式鎖來保證安全;分布式鎖的實現的方式有很多種,最常見的有zookeeper,Redis,數據庫等;zookeeper和redis都需要我們單獨部署甚至搭建集群去提高可用性。這對於服務資源本身不夠的機器來說更是雪上加霜,不過mysql這種作為一個儲存功能應用,我們離不開它,所以用它來實現分布式鎖,不需要額外的去維護一個應用,實現起來也比較簡單,對並發不高項目而言是一種比較好的實現方式;
- 優點:簡單高效可靠
- 缺點:並發性能較低,功能相對來說比較單一
本次演示使用的框架為 SpringBoot+MybatisPlus
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.注意事項
@Transactional(propagation = Propagation.NOT_SUPPORTED)
- 可以看到加鎖的方法上加了事務注解並且配置傳播規則為Propagation.NOT_SUPPORTED(以非事務的方式運行,如果當前存在事務,則掛起當前事務),這樣配置是為了使用手動事務,如果不加上該注解,SpringBoot會自動幫我們加入當前事務,這樣就沒辦法手動提交事務;這樣會導致在並發時,我們的加鎖事務在會等待外部事務一起提交,在默認的隔離級別下面,其他線程的事務是沒辦法讀取未提交的事務,也就是說我們加的鎖數據沒有保存進數據庫,其他線程一樣可以加鎖,這樣就導致加鎖失敗了;
- 加鎖的時候會查詢當前鎖對象tag在表中過期時間最長的那個數據,避免鎖過期沒有釋放,一個tag對應多個值的問題。並且在解鎖的時候需要用主鍵id和tag對應唯一值,刪除了其他加了鎖的數據。
什么時候會出現刪除其他鎖對象的數據:當一個操作執行的時間過長,獲取的鎖已經過期,此時其他同樣需要這個鎖的任務是能夠獲取鎖的,那么此時表中相同的tag數據至少是2條以上,如果不使用主鍵id和tag做唯一標識,那么在釋放鎖的時候就會把別的任務加的鎖一起刪除了,導致其他任務釋放鎖失敗!