سؤال

يرجى إسماء أي أخطاء في المصطلحات. على وجه الخصوص، أنا أستخدم مصطلحات قاعدة البيانات العلائقية.

هناك عدد من متاجر القيمة الرئيسية المستمرة، بما في ذلك البوتشد و كاساندرا, مع الكثير من المشاريع الأخرى.

حجة نموذجية ضدهم هي أنهم لا يسمحون بشكل عام بالمعاملات الذرية عبر صفوف أو جداول متعددة. أتساءل عما إذا كان هناك نهج عام سيحل هذه المشكلة.

خذ على سبيل المثال وضع مجموعة من الحسابات المصرفية. كيف نتحرك الأموال من حساب بنك واحد إلى آخر؟ إذا كان كل حساب مصرفي صف، نريد تحديث صفينين كجزء من نفس المعاملة، مما يقلل من القيمة في واحد وزيادة القيمة في آخر.

نهج واحد واضح هو الحصول على جدول منفصل يصف المعاملات. ثم، فإن نقل الأموال من حساب بنك واحد إلى آخر يتكون من مجرد إدخال صف جديد في هذا الجدول. نحن لا نقوم بتخزين الأرصدة الحالية لأي من الحسابات المصرفية وبدلا من ذلك تعتمد على تلخيص جميع الصفوف المناسبة في جدول المعاملات. من السهل تخيل أن هذا سيكون الكثير من العمل، ولكن قد يكون لدى البنك ملايين المعاملات يوميا وقد يكون له حساب مصرفي فردي بسرعة عدة آلاف من "المعاملات" المرتبطة به.

سيقوم رقم (الكل؟) من متاجر القيمة الرئيسية باتخاذ إجراءات إذا تغيرت البيانات الأساسية منذ أن أمسكت بها آخر مرة. ربما يمكن استخدام ذلك لمحاكاة المعاملات الذرية، ثم، كما يمكنك بعد ذلك الإشارة إلى أن حقل معين مغلق. هناك بعض القضايا الواضحة مع هذا النهج.

أي أفكار أخرى؟ من الممكن تماما أن يكون نهجي غير صحيح ببساطة وأنا لم أتلف ذهني بعد عن طريقة التفكير الجديدة.

هل كانت مفيدة؟

المحلول

إذا كنت تأخذ مثالك، فأنت ترغب في تحديث القيمة بطريقة سحرية في غير مرتبطة المستند (الصف في المصطلحات العلائقية)، يمكنك القيام بذلك في couchdb. ستحصل على خطأ في النزاع عند محاولة الالتزام بالتغيير إذا قام عميل Contenter آخر بتحديث نفس المستند منذ قراءته. سيتعين عليك بعد ذلك قراءة القيمة الجديدة وتحديث وإعادة محاولة الالتزام. هناك غير محدد (ربما لانهائي إذا كان هناك كثيرا من النزول) عدد المرات التي قد تضطر إلى تكرار هذه العملية، لكنك مضمون في الحصول على وثيقة في قاعدة البيانات بميزان محدث بشكل ذرية إذا نجحت ارتكابك من أي وقت مضى.

إذا كنت بحاجة إلى تحديث أرصدة (أي تحويل من حساب واحد إلى آخر)، فأنت بحاجة إلى استخدام مستند معاملات منفصل (بفعالية جدول آخر حيث توجد صفوف معاملات) تخزن المبلغ والحسابين (داخل وخارج) وبعد هذه هي ممارسة مسك الدفاتر الشائعة، بالمناسبة. نظرا لأن COUCHDB يحسب وجهات النظر فقط حسب الحاجة، فمن غير ضروري فعليا فعالا لحساب المبلغ الحالي في حساب من المعاملات التي تسرد هذا الحساب. في CouchDB، ستستخدم وظيفة الخريطة التي تنبعث رقم الحساب كفاة مفاتئة ومقدار المعاملة (إيجابية للوارد السلبي). ستبلكل دالة تقليلك ببساطة القيم لكل مفتاح، ينبعث من نفس المفتاح والمبلغ الإجمالي. يمكنك بعد ذلك استخدام طريقة عرض مع المجموعة = TRUE للحصول على أرصدة الحساب، مع إدخال رقم الحساب.

نصائح أخرى

CouchDB غير مناسب لأنظمة المعاملات لأنه لا يدعم عمليات القفل والعمليات الذرية.

من أجل إكمال التحويل المصرفي، يجب عليك القيام ببعض الأشياء:

  1. التحقق من صحة المعاملة، مما يضمن وجود أموال كافية في الحساب المصدر، أن كلا الحسابات مفتوحة، غير مؤمن، وفي وضع جيد، وهلم جرا
  2. تقليل ميزان الحساب المصدر
  3. زيادة رصيد حساب الوجهة

إذا تم إجراء تغييرات بين أي من هذه الخطوات توازن أو حالة الحسابات، فقد تصبح المعاملة غير صالحة بعد تقديمها وهي مشكلة كبيرة في نظام من هذا النوع.

حتى إذا كنت تستخدم النهج المقترح أعلاه حيث تقوم بإدراج سجل "نقل" واستخدم طريقة عرض خريطة / تقليل لحساب رصيد الحساب النهائي، فلن يكون لديك طريقة لضمان عدم التنافس على الحساب المصدر لأنه لا يزال هناك حالة السباق بين التحقق من رصيد الحساب المصدر وإدراج المعاملة حيث يمكن إضافة معاملتين في وقت واحد بعد التحقق من الرصيد.

لذلك ... إنها أداة خاطئة لهذا المنصب. على الأرجح CouchDB جيدا بكثير من الأشياء، ولكن هذا شيء لا يمكن فعله حقا.

تحرير: ربما تجدر الإشارة إلى أن البنوك الفعلية في العالم الحقيقي تستخدم الاتساق في نهاية المطاف. إذا قمت بالإسراع في حسابك المصرفي لفترة طويلة بما فيه الكفاية، فستحصل على رسوم السحب على المكشوف. إذا كنت جيدا جدا، فقد تكون قادرا على سحب الأموال من أجهزة الصراف الآلي المختلفة في نفس الوقت تقريبا وأوقفت حسابك لأن هناك شرط سباق للتحقق من الرصيد وإصدار المال وتسجيل المعاملة. عند إيداع التحقق في حسابك، فإنهم يضعون الرصيد ولكنهم يحملون هذه الأموال في الواقع لفترة من الوقت "فقط في حالة" الحساب المصدر ليس لديه ما يكفي من المال.

لتوفير مثال ملموس (لأن هناك نقص مفاجئ في الأمثلة الصحيحة عبر الإنترنت): إليك كيفية تنفيذ "تحويل رصيد البنك الذري"في couchdb (نسخ إلى حد كبير من بلدي مدونتي وظيفة على نفس الموضوع: http://blog.codekills.net/2016/03/13/atomic-bank-bance-transfer-with-couchdb/)

أولا، خلاصة موجزة للمشكلة: كيف يمكن تصميم نظام مصرفي يسمح بنقل الأموال بين الحسابات بحيث لا توجد ظروف السباق التي قد تترك أرصدة غير صالحة أو غير تقليدية؟

هناك بعض الأجزاء هذه المشكلة:

أولا: سجل المعاملات. بدلا من تخزين رصيد الحساب في سجل أو مستند واحد - {"account": "Dave", "balance": 100} - يتم احتساب رصيد الحساب من خلال تلخيص جميع الاعتمادات واليدونات لهذا الحساب. يتم تخزين هذه الاعتمادات والخصومات في سجل المعاملات، والتي قد تبدو مثل هذا:

{"from": "Dave", "to": "Alex", "amount": 50}
{"from": "Alex", "to": "Jane", "amount": 25}

ويحلل خريطة Couchdb - تقليل الوظائف لحساب الرصيد شيئا مثل هذا:

POST /transactions/balances
{
    "map": function(txn) {
        emit(txn.from, txn.amount * -1);
        emit(txn.to, txn.amount);
    },
    "reduce": function(keys, values) {
        return sum(values);
    }
}

للكمال، وهنا قائمة الأرصدة:

GET /transactions/balances
{
    "rows": [
        {
            "key" : "Alex",
            "value" : 25
        },
        {
            "key" : "Dave",
            "value" : -50
        },
        {
            "key" : "Jane",
            "value" : 25
        }
    ],
    ...
}

ولكن هذا يترك السؤال الواضح: كيف يتم التعامل مع الأخطاء؟ ماذا يحدث إذا حاول شخص ما إجراء نقل أكبر من رصيدهم؟

مع CouchDB (وقواعد بيانات مماثلة)، يجب تنفيذ هذا النوع من المنطق التجاري ومناولة الأخطاء على مستوى التطبيق. ساذجا، قد تبدو هذه الوظيفة مثل هذا:

def transfer(from_acct, to_acct, amount):
    txn_id = db.post("transactions", {"from": from_acct, "to": to_acct, "amount": amount})
    if db.get("transactions/balances") < 0:
        db.delete("transactions/" + txn_id)
        raise InsufficientFunds()

ولكن لاحظ أنه إذا تعطل التطبيق بين إدراج المعاملة والتحقق من الأرصدة المحدثة، فسيتم ترك قاعدة البيانات في حالة غير متناسقة: قد يتم ترك المرسل بميزان سلبي، والمستلم بالمال الذي لم يكن موجودا سابقا:

// Initial balances: Alex: 25, Jane: 25
db.post("transactions", {"from": "Alex", "To": "Jane", "amount": 50}
// Current balances: Alex: -25, Jane: 75

كيف يمكن إصلاح هذا؟

للتأكد من أن النظام ليس أبدا في حالة غير متناسقة، يجب إضافة قطعتين من المعلومات إلى كل معاملة:

  1. الوقت الذي تم إنشاؤه المعاملة (للتأكد من وجود تامة صارمة الطلب المعاملات)، و

  2. حالة - سواء كانت المعاملة ناجحة أم لا.

سيحتاج أيضا إلى أن يكون هناك عرضين - واحد يقوم بإرجاع الرصيد المتاح للحساب (أي، مجموع جميع المعاملات "الناجحة")، وآخر يؤدي إلى أقدم الصفقة "المعلقة":

POST /transactions/balance-available
{
    "map": function(txn) {
        if (txn.status == "successful") {
            emit(txn.from, txn.amount * -1);
            emit(txn.to, txn.amount);
        }
    },
    "reduce": function(keys, values) {
        return sum(values);
    }
}

POST /transactions/oldest-pending
{
    "map": function(txn) {
        if (txn.status == "pending") {
            emit(txn._id, txn);
        }
    },
    "reduce": function(keys, values) {
        var oldest = values[0];
        values.forEach(function(txn) {
            if (txn.timestamp < oldest) {
                oldest = txn;
            }
        });
        return oldest;
    }

}

قائمة التحويلات قد تبدو الآن مثل هذا:

{"from": "Alex", "to": "Dave", "amount": 100, "timestamp": 50, "status": "successful"}
{"from": "Dave", "to": "Jane", "amount": 200, "timestamp": 60, "status": "pending"}

بعد ذلك، سيحتاج التطبيق إلى الحصول على وظيفة يمكنها حل المعاملات عن طريق التحقق من كل معاملة معلقة من أجل التحقق من أنها صالحة، ثم قم بتحديث حالتها من "معلقة" إما "ناجحة" أو "مرفوضة":

def resolve_transactions(target_timestamp):
    """ Resolves all transactions up to and including the transaction
        with timestamp `target_timestamp`. """
    while True:
        # Get the oldest transaction which is still pending
        txn = db.get("transactions/oldest-pending")
        if txn.timestamp > target_timestamp:
            # Stop once all of the transactions up until the one we're
            # interested in have been resolved.
            break

        # Then check to see if that transaction is valid
        if db.get("transactions/available-balance", id=txn.from) >= txn.amount:
            status = "successful"
        else:
            status = "rejected"

        # Then update the status of that transaction. Note that CouchDB
        # will check the "_rev" field, only performing the update if the
        # transaction hasn't already been updated.
        txn.status = status
        couch.put(txn)

أخيرا، رمز التطبيق ل بشكل صحيح أداء نقل:

def transfer(from_acct, to_acct, amount):
    timestamp = time.time()
    txn = db.post("transactions", {
        "from": from_acct,
        "to": to_acct,
        "amount": amount,
        "status": "pending",
        "timestamp": timestamp,
    })
    resolve_transactions(timestamp)
    txn = couch.get("transactions/" + txn._id)
    if txn_status == "rejected":
        raise InsufficientFunds()

بضعة ملاحظات:

  • من أجل الإيجاز، يفترض هذا التنفيذ المحدد بعض كمية الذرية في خريطة Couchdb - تقليل. تحديث الرمز بحيث لا يعتمد على هذا الافتراض أنه تمرين للقارئ.

  • لم يتم أخذ Sync Master / Master النسخ المتماثل أو وثيقة COUGEDB في الاعتبار. ماجستير / ماجستير النسخ المتماثل والمزامنة تجعل هذه المشكلة أكثر صعوبة بكثير.

  • في نظام حقيقي، باستخدام time() قد يؤدي إلى الاصطدامات، لذلك قد يكون استخدام شيء أكثر مع محرك آخر فكرة جيدة؛ يمكن "%s-%s" %(time(), uuid()), أو استخدام المستند _id في الترتيب. بما في ذلك الوقت ليس ضروريا للغاية، ولكنه يساعد في الحفاظ على منطقي إذا كانت الطلبات متعددة تأتي في نفس الوقت تقريبا.

Berkeleydb و LMDB كلاهما متاجر ذات قيمة رئيسية مع دعم المعاملات الحمضية. في BDB TXNS اختياري أثناء تشغيل LMDB فقط معا معاملة.

حجة نموذجية ضدهم هي أنهم لا يسمحون بشكل عام بالمعاملات الذرية عبر صفوف أو جداول متعددة. أتساءل عما إذا كان هناك نهج عام سيحل هذه المشكلة.

لا تدعم الكثير من متاجر البيانات الحديثة تحديثات التحديثات متعددة الاستخدامات الذرية (المعاملات) خارج الصندوق ولكن معظمها يقدم البدائيات التي تتيح لك بناء المعاملات من جانب العميل الحمضي.

إذا كان مخزن البيانات يدعم القدرة الخطية الرئيسية وقارنات وتبديل التشغيل والاختبار، فهذا يكفي لتنفيذ المعاملات المتسلسلة. على سبيل المثال، يتم استخدام هذا النهج في Percolator في جوجل و في cockroachdb. قاعدة البيانات.

في مدونتي خلقت تصور خطوة بخطوة لمعاملات الجانب الصليب القابل للتسلسل, ووصف حالات الاستخدام الرئيسية وقدمت روابط لمتغير الخوارزمية. آمل أن يساعدك في فهم كيفية تطبيقها لك من متجر البيانات.

من بين مخازن البيانات التي تدعم القدرة الخطية الرئيسية و CAS هي:

  • كاساندرا مع معاملات خفيفة الوزن
  • رياك مع دلاء متسقة
  • Rethinkdb.
  • حارس حديقة الحيوان
  • ETDC.
  • HBase
  • دينامود
  • mongodb.

بالمناسبة، إذا كنت بخير مع مستوى العزلة الملتزنة بعد ذلك، فمن المنطقي إلقاء نظرة على المعاملات المنحدر بواسطة بيتر بريديس. يمكن أيضا تنفيذها أيضا لنفس مجموعة مخازن البيانات.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top