MySQLによる分散ロック

5719 ワード

紹介する
分散システムでは、分散ロックは最も基礎的なツールクラスです.例えば、2つの支払機能を有するマイクロサービスが配備されている場合、ユーザは1つの注文に対して2回の支払操作を開始する可能性があり、この2回の要求は2つのサービスに送信される可能性があるため、重複コミットを分散ロックで防止し、ロックを取得したサービスは正常に支払操作を行い、ロックを取得できないサービスプロンプトの重複操作を取得しなければならない.
当社は大量の基礎ツール類をパッケージしており、分散ロックを使用したい場合は3つのことをします.
1.データベースにgloballocktableテーブルを作成する.対応するjarパッケージ3を導入する.コードに@Autowired GlobalLockComponent globalLockComponentと書いてこのコンポーネントを使用します
この文章を読んでspringboot-starterで同じ機能を実現することもできます.しかし、私たちはこの方法で実現したのではなく、別の文章を開いて、私たちがどのように実現したのかを分析します.
この文章はまずMySQLの分布式の実現を分析します
表を作る

CREATE TABLE `globallocktable` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `lockKey` varchar(60) NOT NULL COMMENT '   ',
 `createTime` datetime NOT NULL COMMENT '    ',
 PRIMARY KEY (`id`),
 UNIQUE KEY `lockKey` (`lockKey`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='   ';


他人に使わせるコンポーネント

@Component
public class GlobalLockComponent {
 @Resource
 GlobalLockTableDAO globalLockDAO;
 /**
  *      ,   true,   false
  */
 public boolean tryLock(String key) {
  return GlobalLockUtil.tryLock(this.globalLockDAO, key);
 }
 /**
  *              ,    timeoutMs(  )  ,          
  *    key     ,     
  */
 public boolean tryLockWithClear(String key, Long timeoutMs) {
  return GlobalLockUtil.tryLockWithClear(this.globalLockDAO, key, timeoutMs);
 }
 /**
  *    ,  key    
  */
 public void releasLock(String key) {
  GlobalLockUtil.releasLock(this.globalLockDAO, key);
 }
}


ロックオブジェクトの定義は次のとおりです.

public class GlobalLockTable {

 private Integer id;
 private String lockKey;
 private Date createTime;
 //   get set  
}

GlobalLockTableDAO    

public interface GlobalLockTableDAO {
 int deleteByPrimaryKey(Integer id);
 int deleteByLockKey(String lockKey);
 GlobalLockTable selectByLockKey(String key);
 int insertSelectiveWithTest(GlobalLockTable record);
}

具体的なロックとロック解除ロジック

public class GlobalLockUtil {
 private static Logger logger = LoggerFactory.getLogger(GlobalLockUtil.class);
 private static GlobalLockTable tryLockInternal(GlobalLockTableDAO lockDAO, String key) {
  GlobalLockTable insert = new GlobalLockTable();
  insert.setCreateTime(new Date());
  insert.setLockKey(key);
  //      1
  int count = lockDAO.insertSelectiveWithTest(insert);
  if (count == 0) {
   GlobalLockTable ready = lockDAO.selectByLockKey(key);
   logger.warn("can not lock the key: {}, {}, {}", insert.getLockKey(), ready.getCreateTime(),
     ready.getId());
   return ready;
  }
  logger.info("yes got the lock by key: {}", insert.getId(), insert.getLockKey());
  return null;
 }
 /**        ,      **/
 public static boolean tryLockWithClear(GlobalLockTableDAO lockDAO, String key, Long timeoutMs) {
  GlobalLockTable lock = tryLockInternal(lockDAO, key);
  if (lock == null) return true;
  if (System.currentTimeMillis() - lock.getCreateTime().getTime() <= timeoutMs) {
   logger.warn("sorry, can not get the key. : {}, {}, {}", key, lock.getId(), lock.getCreateTime());
   return false;
  }
  logger.warn("the key already timeout wthin : {}, {}, will clear", key, timeoutMs);
  //      2
  int count = lockDAO.deleteByPrimaryKey(lock.getId());
  if (count == 0) {
   logger.warn("sorry, the key already preemptived by others: {}, {}", lock.getId(), lock.getLockKey());
   return false;
  }
  lock = tryLockInternal(lockDAO, key);
  return lock != null ? false : true;
 }
 /**    **/
 public static boolean tryLock(GlobalLockTableDAO lockDAO, String key) {
  return tryLockInternal(lockDAO, key) == null ? true : false;
 }
 /**    **/
 public static void releasLock(GlobalLockTableDAO lockDAO, String key) {
  lockDAO.deleteByLockKey(key);
 }
}

このツール類には特に興味深い点が2つありますが、まず注意点を見てください2(上のコードに表示されています)
1.ロックが長時間解放されないように、Redisで実現するとロックタイムアウト時間を設定し、タイムアウト自動解放(後にRedisで分散ロックを実現すると書きます)をMySQLで実現すると先に削除して追加することができます.削除するときにidで削除したものを見ることができますが、nameで削除したものではありません.なぜですか?まず自分で考えてみましょう
nameで削除すると、他の人がこのロックを削除した後、nameでロックを追加した可能性があるので、まだタイムアウト時間になっていないのに、結局あなたはnameに基づいて削除しました.idで削除すると、返されたid=0の場合、他の人がロックを再ロックしたことを説明し、再取得する必要があります.
2.GlobalLockTableオブジェクトdaoレイヤの他の方法はすべて名を知っていて、この方法を見てみましょう.すなわち,コード中の注意点1は,ロックを試みるたびにselectではなく直接insertSelectiveWithTestであり,クエリ時間が少なく効率が向上することを示している.
InsertSelectiveWithTestの役割は、lockKeyが存在する場合に挿入操作を行わず、0を返すことです.ロックキーが存在しない場合は挿入操作を行い、1を返します.


 insert into `globallocktable` (`id`,
 `lockKey`, `createTime` )
  select #{id,jdbcType=INTEGER}, #{lockKey,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP}
  from dual where not exists
  (select 1 from globallocktable where lockKey = #{lockKey,jdbcType=VARCHAR})

使用
私たちが使いたいときは、ビジネスロジックだけ書けばいいので、とても便利です.

if (!globalLockComponent.tryLock(name)) {
 //         
 return;
}
try {
 //        
} catch (Exception e) {
} finally {
 globalLockComponent.releasLock(name)

まとめ
以上述べたように、MySQLを使用して分散ロックを実現し、皆さんの役に立つことを願っています.