rosedbトランザクションの実践


一、前言
トランザクションは、Mysql、Oracle、PostgreSqlなど、従来のリレーショナル・データベースでは欠かせない機能ですが、NoSQLデータベースではトランザクションの概念が弱く、実装上はリレーショナル・データベースほど複雑ではありません.
しかし、データの完全な一貫性のために、ほとんどのk-vはトランザクションの基本的な特性を実現します.例えば、k-vデータベースの2つの元祖であるLevelDBとRocksDB、いくつかのGo言語で実装されたオープンソースk-vも、Bolt、Badgerなどのトランザクションをサポートしています.
rosedbのトランザクションは現在、初級バージョンを実現したばかりで、コードはまだ簡単ですが、私の予想した構想の中で、後続は徐々に複雑になる可能性があります.
説明しなければならないのは、rosedbの事務を実現する前に、私の事務に対する理解もACIDという基礎概念に限られていたので、今回の実現は完全に石を触って川を渡るので、いくつかの溝があるかもしれません.何か疑問があれば、私も後で勉強して改善します.
二、基本概念
トランザクションといえば、トランザクションのACID特性を思い浮かべやすいので、振り返ってみましょう.
  • 原子性(Atomicity):1つのトランザクションのすべての操作は、すべて完了するか、すべて失敗し、中間段階では終了しません.トランザクションの実行中にエラーが発生した場合、トランザクションが開始される前の状態にロールバックできます.
  • コンシステンシ(Consistency):トランザクションの開始前と終了後にデータベースの整合性が損なわれず、データ・ステータスが常に予想通りであることを意味します.
  • 独立性(Isolation):独立性は、複数の実行中のトランザクションの相互影響の程度を記述し、トランザクション間の異なる影響の程度を表す一般的な4つの独立性レベルがあります.
  • リード未コミット(read uncommitted):1つのトランザクションがまだコミットされていない場合、別のトランザクションが変更された(ダーティリードが存在する)
  • が表示されます.
  • リードコミット(read committed):1つのトランザクションによるデータの変更は、コミット後にのみ他のトランザクションが表示される(ダーティリードはありませんが、重複リードはできません)
  • .
  • リピート可能(repeatable read):1つのトランザクションが実行中に取得したデータは、トランザクションの開始時のデータと一致します(汚れた読み取りはなく、リピートできますが、幻の読み取りがあります)
  • .
  • シリアル化(serializable):読み書き反発、トランザクションの同時発生を回避します.トランザクションは、前のトランザクションがコミットされるまで待たなければなりません(汚れのない読み取り、繰り返し読み取り、幻読み取りなし)
  • .
  • 持続性(Durability):トランザクションがコミットされた後、データベースがクラッシュしてもセキュリティが保証される変更は永続的です.

  • ACIDの概念は多いように見えますが、理解しにくいわけではありません.トランザクションを実現するには、データの読み書き時にトランザクションを満たすことを保証する基本的な概念です.AIDは保証しなければなりません.
    Consistencyは一貫性であり、それがトランザクションの最終目標であることを簡単に理解することができ、データベースはAIDを通じて一貫性を保証し、私たちは応用面でも一貫性を保証しなければならない.もし私たちが書いたデータ自体が論理的に間違っていたら、データベースの事務がどんなに完備していても、一貫性を保証することはできない.
    三、具体的な実現
    トランザクションの実装について説明する前に、rosedbでのトランザクションの基本的な使い方を見てみましょう.
    //        
    db, err := rosedb.Open(rosedb.DefaultConfig())
    if err != nil {
       panic(err)
    }
    
    //         
    err = db.Txn(func(tx *Txn) (err error) {
       err = tx.Set([]byte("k1"), []byte("val-1"))
       if err != nil {
          return
       }
       err = tx.LPush([]byte("my_list"), []byte("val-1"), []byte("val-2"))
       if err != nil {
          return
       }
       return
    })
    
    if err != nil {
       panic(fmt.Sprintf("commit tx err: %+v", err))
    }

    まず、データベース・インスタンスが開き、Txnメソッドが呼び出されます.このメソッドのパラメータは関数であり、トランザクションの操作はすべてこの関数で完了し、コミット時に一度に実行されます.
    このように使用すると、トランザクションは自動的にコミットされます.もちろん、手動でトランザクションを開いてコミットすることもできます.エラーが発生した場合、手動でロールバックします.次のようにします.
    //        
    db, err := rosedb.Open(rosedb.DefaultConfig())
    if err != nil {
       panic(err)
    }
    
    //     
    tx := db.NewTransaction()
    err = tx.Set([]byte("k1"), []byte("val-1"))
    if err != nil {
       //         
       tx.Rollback()
       return
    }
    
    //     
    if err = tx.Commit(); err != nil {
       panic(fmt.Sprintf("commit tx err: %+v", err))
    }

    もちろん、手動でトランザクションをコミットしたり、ロールバックしたりするのを省く最初の使い方をお勧めします.Txnメソッドは読み書きトランザクションを表し、またTxnViewメソッドは読み取り専用トランザクションを表し、使用方法は完全に一致しているが、TxnViewメソッド内の書き込みコマンドは無視される.
    db.TxnView(func(tx *Txn) error {
       val, err := tx.Get([]byte("k1"))
       if err != nil {
          return err
       }
       //    val
    
       hVal := tx.HGet([]byte("k1"), []byte("f1"))
       //    hVal
      
       return nil
    })

    トランザクションのACIDの基本概念とrosedbトランザクションの基本的な使い方を理解した後、rosedbでトランザクションがどのように実現されているのか、AIDの特性をどのように保証しているのかを見てみましょう.
    3.1原子性
    前述したように、原子性とは、トランザクションの実行の完全性を指し、すべて成功するか、すべて失敗し、中間状態にとどまることはできません.
    原子性を実現するのは難しくないが,rosedbの書き込み特性によって解決できる.まずrosedbデータの書き込みの基本的な流れを振り返ります.2つのステップは、まずデータがディスクに落ち、信頼性が保証され、メモリのインデックス情報が更新されます.
    トランザクション・オペレーションの場合、原子性を保証するには、書き込みが必要なデータをメモリに一時保存し、トランザクションをコミットするときにディスク・ファイルに一度に書き込むことができます.
    ディスクへの一括書き込み中にエラーが発生したり、システムがクラッシュしたりしたらどうするかという問題があります.つまり、書き込みに成功したデータもあれば、書き込みに失敗したデータもあるかもしれません.原子的な定義に従って、今回のトランザクションはコミットされず、無効ですが、書き込まれたデータが無効であることをどのように知るべきですか?
    現在rosedbは最も理解しやすく、比較的簡単な方法でこの問題を解決している.
    具体的には、トランザクションが開始されるたびにグローバル一意のトランザクションidが割り当てられ、書き込む必要があるデータがこのトランザクションidを持ってファイルに書き込まれます.すべてのデータのディスクへの書き込みが完了したら、このトランザクションidを個別に保存します(ファイルにも書き込みます).データベースが起動すると、このファイルのすべてのトランザクションidがロードされ、コミットされたトランザクションidと呼ばれるコレクションに維持されます.
    これにより、一括書込み時にデータがエラーになっても、対応するトランザクションidが格納されていないため、データベースが起動してデータ構築インデックスを取り出した場合(rosedbの起動プロセスを思い出して)、データに対応するトランザクションidがコミット済トランザクションidセットに含まれていないことを確認できるので、これらのデータは無効とみなされる.
    ほとんどのLSM流派のk-vは、トランザクションの原子性を保証するために同様の考え方を利用しています.たとえばrocksdbは、トランザクション内のすべての書き込みをWriteBatchに格納し、トランザクションがコミットされたときに一度に書き込みます.
    3.2隔離性
    現在rosedbでは、読み書きトランザクションと読み取り専用トランザクションの2つのトランザクションタイプがサポートされています.1つの読み書きトランザクションのみを同時に開くことができ、読み取り専用トランザクションは複数のトランザクションを同時に開くことができます.
    このモードでは、読み書きにリードロック、書き込みにライトロックがかかります.つまり、読み書きは反発し、同時に行うことはできません.これは4つの独立性レベルにおけるシリアル化であり,その利点は簡単で実現しやすいことであり,欠点は同時能力が悪いことであると理解できる.
    説明する必要があるのは、現在のこのような実装は後で大体率が調整されることであり、スナップショット分離方式を使用して読み取りコミットまたは繰り返し読み取りをサポートすることができることを想定している.このようにデータ読み取りは履歴バージョンまで読むことができ、書き込み操作のブロックをもたらすことはないが、実装上はずっと複雑である.
    3.3持続性
    永続性保証データは、最も一般的なディスクやSSDなど、不揮発性記憶媒体に記載されており、システム異常が発生してもデータの安全を保証することができます.
    rosedbでは、データを書き込むときにデフォルトのブラシポリシーを実行すると、オペレーティングシステムのページキャッシュにデータを書き込み、実際にはディスクが落ちていません.オペレーティングシステムがページキャッシュのデータをディスクにブラシするのに間に合わない場合は、データが失われます.これにより、永続性を完全に保証することはできませんが、Syncブラシディスクは極めて遅い動作であるため、パフォーマンスは比較的良好です.
    rosedbを起動するときにコンフィギュレーション・アイテムSyncをtrueと指定すると、書き込みのたびにSyncが強行され、データが失われないことが保証されますが、書き込み性能が低下します.
    実際にどのように選択すれば、自分の使用シーンに応じて、システムが安定し、性能に対する要求が高く、少量のデータの損失を許容できる場合は、デフォルトのポリシー、すなわちSyncがfalseであり、そうでなければブラシディスクを強制することができます.
    四、欠陥
    上記の簡単な分析からrosedbはトランザクションのAID特性を基本的に実現していることがわかり、全体的には簡単で、学習と使用が容易で、さらに拡張しやすいことがよく理解できます.もちろん、現在もいくつかの欠陥が解決されなければならない.
    1つ目は、上述した独立性レベルの問題です.現在、この方法は簡単すぎて、グローバルな大きなロックを使用してシリアル化されています.その後、操作が必要なキーだけをロックし、ロックの粒度を小さくすることを考慮することができます.
    もう1つの問題はrosedbは複数のデータ構造をサポートしているが、List、ZSetのような構造では、トランザクションですべてのコマンドをサポートするのは難しいため、現在、ListはLPushとRPushのみをサポートし、ZSetはZAdd、ZScore、ZRemコマンドのみをサポートしている.
    主な理由は,既存のkeyをトランザクションで読み書きすると,範囲検索のようなタイプのコマンドをサポートすることが困難になるためであり,比較的良い解決策はまだ考えられていない.
    最後に、プロジェクトの住所:https://github.com/roseduan/rosedbを添付します.皆さん、ツッコミを見に来てください.
    Ps:rosedbもストレージ、k-vに興味のある友人の参加を歓迎し、私の微信を加えて深く検討したり交流したりすることができます.