1. 程式人生 > 其它 >使用MySQL實現分散式鎖

使用MySQL實現分散式鎖

分散式鎖我們經常使用,在我們專案多節點部署或者微服務專案中經常使用;使用的方式有很多種,最常見的有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做唯一標識,那麼在釋放鎖的時候就會把別的任務加的鎖一起刪除了,導致其他任務釋放鎖失敗!