مقدمة
من السهل التعامل مع التزامن داخل JVM واحد. ما عليك سوى استخدام القفل الذي توفره JDK مباشرة ، ولكن التزامن عبر العملية أمر مستحيل بالتأكيد. في هذه الحالة ، يجب أن تعتمد على طرف ثالث. أستخدم Redis هنا ، وبالطبع هناك العديد من أساليب التنفيذ الأخرى. في الواقع ، فإن المبدأ القائم على تنفيذ Redis بسيط للغاية. قبل قراءة الرمز ، يوصى بالتحقق من المبدأ أولاً. بعد قراءة الكود ، يجب أن يكون من الأسهل فهمها.
أنا لا أقوم بتطبيق واجهة JDK's java.util.concurrent.locks.Lock ، ولكن تخصيص واحدة ، لأن هناك طريقة newCondition في JDK. أنا لم نفذها في الوقت الحالي. يوفر هذا القفل 5 متغيرات من طرق القفل. يمكنك اختيار أي واحد لاستخدامه للحصول على القفل. فكرتي هي أنه من الأفضل استخدام الأساليب مع عودة المهلة. لأنه إذا لم يكن هذا هو الحال ، إذا تم تعليق Redis ، فسيكون الخيط دائمًا في الحلقة الميتة (حول هذا الموضوع ، فيجب أن يتم تحسينه بشكل أكبر. إذا تم تعليق Redis ، فإن عملية Jedis ستعرض بالتأكيد استثناءات وما إلى ذلك. يمكنك تحديد آلية لإخطار المستخدمين الذين يستخدمون هذا القفل عندما يتم تعليق Redis ، أو مؤشرات الترابط)
حزمة cc.lixiaohui / ** * حظر قفل الاستحواذ ، وليس الاستجابة للمقاطعة * * throws interruptedException */ void lockinterruptively interruptedexception ؛ / *** حاول الحصول على القفل ، والعودة فورًا دون الحصول عليه ، وعدم حظر*/ boolean trylock ؛ / ** * قفل اكتساب الحظر الذي يتم إرجاعه تلقائيًا عن طريق المهلة ، وعدم الاستجابة للمقاطعة * * param time * param unit * return {code true} إذا تم الحصول على القفل بنجاح ، {code false} إذا لم يتم استرداد القفل خلال الوقت المحدد * */ boolean trylock (time time ، وحدة timeUnit) ؛ / *** قفل الاستحواذ الحظر الذي تم إرجاعه تلقائيًا عن طريق المهلة ، مقاطعة الاستجابة** param time* param unit* return {code true} InterruptedException ؛ / *** قفل إطلاق*/ إلغاء تأمين باطل ؛ }انظر إلى تنفيذها التجريدي:
Package cc.lixiaohui * * Author lixiaohui * * / / / / /تجريبي عام تجريبي ، قم بتنفيذ قفل { /** * <pre> * ما إذا كانت الرؤية تحتاج إلى ضمان هنا تستحق المناقشة ، لأنها قفل موزع ، * 1. مضمون. * </pre> */ محمية مقفلة منطقية متقلبة ؛ / ** * الخيط الذي يحمل القفل حاليًا في JVM (إذا كان لديك واحد) */ Private Thread ExclusionOwnerThread ؛ قفل الفراغ العام {try {lock (false ، 0 ، null ، false) ؛ } catch (interruptedException e) {// todo reghore}} رميات lockinterruptlyly interruptedException {lock (false ، 0 ، null ، true) ؛ } trylock boolean العامة (وقت طويل ، وحدة الوقت) {try {return lock (true ، time ، unit ، false) ؛ } catch (interruptedException e) {// todo تجاهل} return false ؛ } يلقي Boolean TryOclOckInterruptly (الوقت الطويل ، وحدة الوقت) InterruptedException {Return Lock (True ، Time ، Unit ، True) ؛ } public void inlock {// todo تحقق مما إذا كان مؤشر الترابط الحالي يحمل القفل إذا (thread.currentThread! = getExClusiveOwnerThread) {رمي جديد intervalonitorstateException ("الخيط الحالي لا يحمل القفل") ؛ } unlock0 ؛ setExClusiveOwnerThread (NULL) ؛ } setExClusiveOwnerThread (موضوع مؤشر ترابط)} محمي void } مؤشر الترابط النهائي المحمي getExClusiveOwnerThread {return ExclusionOwnerThread ؛ } محمية مجردة void unlock0 ؛ / ** * تنفيذ حظر قفل الاستحواذ * * param useTimeout * param time * param unit * param مقاطعة ما إذا كان للرد على المقاطعات * @therrows interruptededexception ؛ استنادًا إلى التنفيذ النهائي لـ Redis ، يكون الرمز الرئيسي لاكتساب القفل وإطلاقه في طريقة lock وطريقة unlock0 هذا الفئة. يمكنك فقط إلقاء نظرة على هاتين الطريقتين وكتابة واحدة بنفسك تمامًا:
package cc.lixiaohui.lock ؛ import java.util.concurrent.timeunit ؛ import redis.clients.jedis.jedis ؛/** * <pre> * * توزيع القفل الذي تم تنفيذه بواسطة عملية setNx استنادًا إلى redis * * من الأفضل استخدام القفل (الوقت الطويل ، وحدة الوقت) href = "http://redis.io/commands/setnx"> مرجع تشغيل setnc </a> * </pre> * * Author lixiaohui * */public class redisBasedDistributedLock يمتد Abstractlock {private jedis jedis ؛ // اسم القفل المحمي القفل. // مدة صحة القفل (MS) محمي lockexpires الطويل ؛ public redisBasedDistributedLock (Jedis Jedis ، String Lockkey ، Long lockexpires) {this.jedis = jedis ؛ this.lockkey = lockkey ؛ this.lockexires = lockexpires ؛ } // تنفيذ حظر قفل الاستحواذ المحمي القفل المنطقي (منطقية USETIMEOUT ، وقت طويل ، وحدة الوقت ، مقاطعة منطقية) يلقي InterruptedException {if (interrupt) {checkInterruption ؛ } start = system.currentTimeMillis ؛ timeout long = unit.tomillis (time) ؛ // if! useTimeout ، ثم لا جدوى منه (useTimeout؟ isTimeOut (start ، timeout): true) {if (interrupt) {checkInterruption ؛ } long lockexpiretime = system.currentTimeMillis + lockexpires + 1 ؛ // قفل string stringoflockexpiretime = string.valueof (lockexpiretime) ؛ if (jedis.setnx (lockkey ، stringoflockexpiretime) == 1) {// تم الحصول على قفل // TODO بنجاح ، وضبط المعرف ذي الصلة Locked = true ؛ setExClusiveOwnerThread (thread.currentThread) ؛ العودة صحيح. } قيمة السلسلة = jedis.get (lockkey) ؛ إذا (value! = null && isTimeExpired (value)) {// قفل انتهت صلاحيته // افترض أن مؤشرات ترابط متعددة (JVM non-single) تأتي هنا في نفس الوقت oldvalue = jedis.getset (lockkey ، stringoflockexperetime) ؛ // getset هو ذري // لكن القديم الذي تم الحصول عليه من قبل كل مؤشر ترابط عندما يأتي هنا من المستحيل بالتأكيد أن يكون هو نفسه (لأن getset ذري) // لا يزال منتهي الصلاحية القديمة التي تم الحصول عليها بالانضمام ، فهذا يعني أن القفل يتم الحصول عليه إذا كان ذلك (oldvalue! = null && istimexpired (oldvalue) {{set the set setExClusiveOwnerThread (thread.currentThread) ؛ العودة صحيح. }} آخر {// todo lock غير منتهية الصلاحية ، أدخل إعادة Retrying}} false ؛ } public boolean trylock {long lockexpiretime = system.currentTimeMillis + lockexpires + 1 ؛ // timeout time stringoflockexperetime = string.valueof (lockexpiretime) ؛ if (jedis.setnx (lockkey ، stringoflockexpiretime) == 1) {// الحصول على lock // todo يكتسب القفل بنجاح ، اضبط المعرف ذي الصلة locked = true ؛ setExClusiveOwnerThread (thread.currentThread) ؛ العودة صحيح. } قيمة السلسلة = jedis.get (lockkey) ؛ إذا كان (القيمة! = null && isTimeExpired (value)) {// قفل انتهت صلاحيته // افترض أن عدة مؤشرات ترابط (وليس JVM واحد) تأتي إلى هنا في نفس الوقت oldvalue = jedis.getset (lockkey ، stringoflockexpiretime) ؛ // getset هو ذري // لكن أولفالوي الذي تم الحصول عليه من قبل كل موضوع عندما يأتي هنا أمر مستحيل بالتأكيد (لأن getset هو ذري) // إذا كان القديم الذي تحصل عليه لا يزال منتهي الصلاحية ، فهذا يعني أنك قد حصلت على القفل (OldValue! = null && IstimeExpired (oldvalue)) setExClusiveOwnerThread (thread.currentThread) ؛ العودة صحيح. }} آخر {// todo lock غير منتهية الصلاحية ، أدخل Retrying to -loop} إرجاع false ؛ } /*** استفسارات إذا تم الاحتفاظ بهذا القفل بواسطة أي موضوع. * * regurn {code true} إذا كان أي مؤشر ترابط يحمل هذا القفل و * {code false} خلاف ذلك */ public boolean islocked {if (locked) {return true ؛ } آخر {string value = jedis.get (lockkey) ؛ // TODO هناك بالفعل مشكلة هنا. فكر: عندما تقوم طريقة GET بإرجاع القيمة ، افترض أن القيمة قد انتهت ، // في هذه اللحظة ، تقوم عقدة أخرى بتعيين القيمة ، ويتم الاحتفاظ بالقفل بواسطة مؤشر ترابط آخر (تمسك العقدة) ، ولا يمكن للحكم التالي // اكتشاف هذا الموقف. ومع ذلك ، لا ينبغي أن تتسبب هذه المشكلة في مشاكل أخرى ، لأن الغرض من هذه الطريقة هو // ليس تحكمًا متزامنًا ، فهو مجرد تقرير عن حالة القفل. إرجاع! isTimeExpired (القيمة) ؛ }} Override محمية void inlock0 {// todo يحدد ما إذا كان القفل ينتهي قيمة السلسلة = jedis.get (lockkey) ؛ if (! isTimeExpired (value)) {dounlock ؛ }} رميات checkIntertureded private void interruptedException {if (thread.currentThread.isinterrupted) {throw new interruptedException ؛ }} private boolean isTimeExPired (قيمة السلسلة) {return long.parselong (value) <system.currentTimeMillis ؛ } boolean private isTimeOut (start arg ، ang timeout) {return start + timeout> system.currentTimeMillis ؛ } private void dounlock {jedis.del (lockkey) ؛ }} إذا قمت بتغيير طريقة التنفيذ في المستقبل (مثل zookeeper ، إلخ) ، فيمكنك أن ترث AbstractLock مباشرة وتنفيذ L ock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt) وطريقة unlock0 (ما يسمى بالتجريد)
امتحان
محاكاة مزارع المعرف العالمي وتصميم فئة IDGenerator . هذه الفئة مسؤولة عن توليد معرفات تدريجية عالمية. رمزه كما يلي:
Package cc.lixiaohui قفل القفل النهائي الخاص Gery Private Static BigInteger Geriment = biginteger.valueof (1) ؛ idgenerator العامة (قفل قفل) {this.lock = lock ؛ } السلسلة العامة getAndIncrement {if (lock.trylock (3 ، timeUnit.seconds)) {try {// todo الحصول على القفل هنا والوصول إلى مورد المنطقة الحرجة getAndIncrement0 ؛ } أخيرًا {lock.unlock ؛ }} الإرجاع null ؛ // return getandincrement0 ؛ } سلسلة خاصة getAndIncrement0 {string s = id.toString ؛ id = id.add (زيادة) ؛ العودة s ؛ }} اختبار المنطق الرئيسي: افتح خيطين في نفس JVM للحلقات المميتة (لا يوجد فاصل بين الحلقات ، إذا كان هناك واحد ، فإن الاختبار سيكون بلا معنى) للحصول على ID (أنا لست حلقة ميتة ولكن الركض لمدة 20s) ، احصل على المعرف وتخزينه في نفس Set . قبل تخزينه ، تحقق مما إذا كان ID موجودًا في set . إذا كان موجودًا بالفعل ، فدع كلا الموضوعين يتوقفان. إذا تمكن البرنامج من تشغيل 20 ثانية بشكل طبيعي ، فهذا يعني أن هذا القفل الموزع يمكن أن يفي بالمتطلبات. يجب أن يكون تأثير مثل هذا الاختبار هو نفسه تأثير JVMs المختلفة (أي في بيئة موزعة حقيقية). فيما يلي رمز فئة الاختبار:
package cc.lixiaohui.distributedlock.distributedlock ؛ استيراد java.util.hashset ؛ استيراد java.util.set ؛ استيراد org.junit.test ؛ redis.clients.jedis.jedis ؛ import cc.lixiaohui.lock.idgenerator ؛ import cc.lixiaohui.lock.lock.lock.lock.lock ؛ cc.lixiaohui.lock.redisBasedDistributedLock ؛ فئة عامة IdGenerAtortest {مجموعة ثابتة خاصة <string> endrendedIds = new hashset <string> ؛ Static Final String Lock_key = "lock.lock" ؛ خاص ثابت نهائي LOCK_EXPIRE = 5 * 1000 ؛ Test Public Void Test Test InterruptedException {Jedis Jedis1 = New Jedis ("LocalHost" ، 6379) ؛ lock1 = جديد redisBasedDistributedLock (jedis1 ، lock_key ، lock_expire) ؛ IdGenerator G1 = idgenerator جديد (lock1) ؛ idconsumemission strupe1 = idconsumemission جديد (G1 ، "الاستهلاك 1") ؛ Jedis Jedis2 = New Jedis ("LocalHost" ، 6379) ؛ lock2 = جديد redisBasedDistributedLock (jedis2 ، lock_key ، lock_expire) ؛ IdGenerator G2 = idgenerator جديد (lock2) ؛ idconsumemission الاستهلاك 2 = idconsumemission جديد (G2 ، "الاستهلاك 2") ؛ الموضوع T1 = مؤشر ترابط جديد (الاستهلاك 1) ؛ الموضوع T2 = مؤشر ترابط جديد (الاستهلاك 2) ؛ t1.start ؛ t2.start ؛ thread.sleep (20 * 1000) ؛ // دع اثنين من المواضيع تعمل لمدة 20 ثانية idconsumemission.stop ؛ T1.oin ؛ t2. join ؛ } static string time {return string.valueof (System.CurrentTimeMillis / 1000) ؛ } فئة ثابتة idconsumemission تنفذ Runnable {private IdGenerator IdGenerator ؛ اسم السلسلة الخاصة ؛ توقف منطقي ثابت ثابت ثابت ؛ IdConsumemission العامة (IdGenerator IdGenerator ، اسم السلسلة) {this.idgenerator = idgenerator ؛ this.name = name ؛ } توقف باطل ثابت عام {stop = true ؛ } public void run {system.out.println (time + ": استهلاك" + name + "start") ؛ بينما (! توقف) {string id = idgenerator.getandIncrement ؛ if (enderatedids.contains (id)) {system.out.println (time + ": معرف مكرر تم إنشاؤه ، id =" + id) ؛ توقف = صحيح ؛ يكمل؛ } enderendids.add (id) ؛ system.out.println (time + ": استهلاك" + name + "إضافة id =" + id) ؛ } system.out.println (time + ": استهلاك" + name + "done") ؛ }}}لكي أكون واضحا ، فإن الطريقة التي أتوقف بها اثنين من الخيوط هنا ليست جيدة جدا. لقد فعلت ذلك للراحة ، لأنه مجرد اختبار ، لذلك من الأفضل عدم القيام بذلك.
نتائج الاختبار
هناك الكثير من الأشياء المطبوعة في العشرينات. يتم clear تلك المطبوعة في المقدمة ومتاحة فقط عند الانتهاء من التشغيل تقريبًا. لقطة الشاشة أدناه. هذا يدل على أن هذا القفل يعمل بشكل طبيعي:
عندما لا يتم قفل IDGererator (أي ، فإن طريقة getAndIncrement الخاصة بـ IDGererator لا تقفلها عندما تحصل على id داخليًا) ، ولن يمر الاختبار ، وهناك احتمال كبير جدًا بأنه سيتوقف في منتصف الطريق. فيما يلي نتائج الاختبار عندما لا يكون القفل مغلقًا:
هذا يستغرق أقل من ثانية واحدة:
هذا واحد يستغرق أقل من ثانية واحدة:
خاتمة
حسنًا ، ما ورد أعلاه هو كل شيء عن تنفيذ الأقفال الموزعة Java بناءً على redis. إذا وجدت أي مشاكل ، فأنت تأمل في تصحيحها. آمل أن يساعدك هذا المقال في الدراسة والعمل. إذا كان لديك أي أسئلة ، فيمكنك ترك رسالة للتواصل.