使用MySQL實現分布式鎖


image

分布式鎖開發中經常使用,在項目多節點部署或者微服務項目中,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做唯一標識,那么在釋放鎖的時候就會把別的任務加的鎖一起刪除了,導致其他任務釋放鎖失敗!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM