1. 程式人生 > 實用技巧 >利用DB實現一個業務鎖例子

利用DB實現一個業務鎖例子

由於網際網路的發展,現在大部分專案都是分散式架構多機部署的,為了防止某些需要單一執行的操作同時被多臺伺服器執行了,就需要在這些業務點上加上分散式鎖,來防止多伺服器的併發和重複job。常見的場景比方說給使用者傳送一些訊息,如果同一時刻N臺伺服器獲取到了任務,那麼結果將不堪設想,很容易對使用者形成了資訊轟炸的效果,如果再觸發一些限流機制,那直接影響後續的業務進行了。
下面這個例子呢,只是為了更好的理解鎖的概念,生產中常用的鎖可以使用redis的分散式鎖機制。

鎖模型SQl

CREATE TABLE `lock` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `key` varchar(128) NOT NULL COMMENT '鎖唯一性Key',
  `create_time` datetime NOT NULL COMMENT '建立時間',
  `expire_time` bigint(20) unsigned NOT NULL COMMENT '鎖過期時間戳',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_lock` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

簡單的鎖模型

@TableName("lock")
@Data
public class LockEntity {
 /**
  * 自增的id,當解鎖的時候做刪除操作時用作樂觀鎖條件
  */
 @Version
 private Long id;
 /**
  * 鎖的key,唯一
  */
 private String key;
 /**
  * 過期時間
  */
 private Long expireTime;
 /**
  * 建立時間
  */
 private Date createTime;
 /**
  * db的當前時間,用於判斷是否過期
  */
 private Long dbTime;
}

提供鎖能力的Service

public interface LockService {
    /**
     * 嘗試獲得鎖
     */
    LockEntity tryLock(String key, Long seconds);

    /**
     * 解鎖
     */
    Boolean unLock(String key, Long lockId);
}

具體實現ServiceImpl

@Service
public class LockServiceImpl implements LockService {
    @Autowired
    private LockMapper lockMapper;

    @Override
    public LockEntity tryLock(String key, Long seconds) {
        try {
            return lock(key, seconds);
        } catch (Exception e) {
            //獲得鎖過程中出錯了
            return null;
        }
    }

    private LockEntity lock(String key, Long seconds) {
        //每次上鎖之前先校驗
        LockEntity lockEntity = lockMapper.selectByKey(key);
        /**
         * 如果鎖存在了
         * 判斷過期時間,如果沒有過期,說明正在有執行緒持有鎖
         * 如果已經過期了,說明由於某種原因沒有釋放鎖
         */
        if (lockEntity != null) {
            if (lockEntity.getDbTime() < lockEntity.getExpireTime()) {
                return null;
            } else {
                /**
                 * 如果多個執行緒刪除同一個鎖,必須用id作為樂觀鎖進行操作
                 * 哪個執行緒刪除成功了,就有獲得鎖的權力
                 */
                if (!removeLock(key, lockEntity.getId())) {
                    return null;
                }
            }
        }
        lockMapper.insert(key, new Date(), seconds);
        return lockMapper.selectByKey(key);
    }

    @Override
    public Boolean unLock(String key, Long lockId) {
        return removeLock(key, lockId);
    }

    private boolean removeLock(String key, Long id) {
        return lockMapper.delete(Wrappers.<LockEntity>lambdaQuery()
                .eq(LockEntity::getKey, key)
                .eq(LockEntity::getId, id)) > 0;
    }

}

其他的一些sql(由於僅僅作為筆記,所以沒有按照mybatis規範去實現持久層程式碼)

@Mapper
public interface LockMapper extends BaseMapper<LockEntity> {

    String columns = "id,key,create_time as createTime,expire_time as expireTime,unix_timestamp(now()) as dbTime ";

    @Select("select " + columns + "form lock where #{key}")
    LockEntity selectByKey(String key);

    @Insert("insert into lock (key,create_time,expire_time) values (#{key,#{createTime},unix_timestamp(now())+#{expireTime})")
    void insert(String key, Date createDate,Long expireTime);
}

這個小例子呢,其實是利用了mysql的唯一主鍵的排他性,將DB的排他能力運用到了我們的業務服務中。
此外,瞭解了這樣的思路之後,我們是不是也可以用一些其他第三方的服務特性來實現業務鎖呢,比如服務註冊中心,服務的註冊id也是唯一的,所以上鎖的過程其實就是向服務註冊中心註冊服務,誰成功了,誰就相當於獲得鎖了。