คำนำ
การซิงโครไนซ์ภายใน JVM เดียวนั้นง่ายต่อการจัดการ เพียงแค่ใช้ล็อคที่จัดทำโดย JDK โดยตรง แต่การซิงโครไนซ์ข้ามกระบวนการเป็นไปไม่ได้อย่างแน่นอน ในกรณีนี้คุณต้องพึ่งพาบุคคลที่สาม ฉันใช้ Redis ที่นี่และแน่นอนว่ามีวิธีการใช้งานอื่น ๆ อีกมากมาย ในความเป็นจริงหลักการที่ใช้การใช้งาน Redis นั้นค่อนข้างง่าย ก่อนที่จะอ่านรหัสขอแนะนำให้คุณตรวจสอบหลักการก่อน หลังจากอ่านรหัสแล้วควรเข้าใจง่ายกว่า
ฉันไม่ได้ใช้งาน java.util.concurrent.locks.Lock ของ jdk แต่ปรับแต่งอย่างหนึ่งเพราะมีวิธี newCondition ใน JDK ฉันยังไม่ได้ดำเนินการในขณะนี้ ล็อคนี้ให้วิธีล็อค 5 รุ่น คุณสามารถเลือกที่จะใช้เพื่อรับล็อค ความคิดของฉันคือเป็นการดีที่สุดที่จะใช้วิธีการด้วยการกลับหมดเวลา เพราะถ้าไม่เป็นเช่นนั้นหาก Redis ถูกแขวนไว้เธรดจะอยู่ในวงที่ตายแล้วเสมอ (เกี่ยวกับเรื่องนี้ควรได้รับการปรับให้เหมาะสมต่อไปหาก Redis ถูกแขวนการดำเนินการของ Jedis จะทำการยกเว้นอย่างแน่นอนและอื่น ๆ คุณสามารถกำหนดกลไกเพื่อแจ้งผู้ใช้ที่ใช้ล็อคนี้เมื่อ Redis ถูกแขวน
แพ็คเกจ cc.lixiaohui.lock; นำเข้า java.util.concurrent.timeUnit; ล็อคอินเตอร์เฟสสาธารณะ { / *** การปิดกั้นล็อคการเข้าซื้อกิจการไม่ตอบสนองต่อการขัดจังหวะ* / void lock; / ** * การปิดกั้นล็อคการเข้าซื้อกิจการไม่ตอบสนองต่อการขัดจังหวะ * * @throws interruptedException */ void lockinterructibly โยน interruptedException; / *** พยายามรับล็อคกลับทันทีโดยไม่ได้รับมันไม่ปิดกั้น*/ บูลีนทริลล็อค; / ** * ล็อคการเข้าซื้อกิจการที่ถูกส่งกลับโดยอัตโนมัติโดยการหมดเวลาไม่ตอบสนองต่อการขัดจังหวะ * * @param เวลา * @param unit * @return {@code true} หากล็อคได้สำเร็จ {@code false} หากการล็อคไม่ได้เรียกคืนภายในเวลาที่กำหนด * */ boolean trylock / *** ล็อคการเข้ามาของการปิดกั้นโดยอัตโนมัติส่งคืนโดยหมดเวลาตอบสนองขัดจังหวะ** @param เวลา* @param unit* @return {@code true} หากล็อคได้รับสำเร็จ {@code false} หากการล็อคนั้นไม่ได้ใช้เวลานาน พ่น InterruptedException; / *** ปลดล็อค*/ โมฆะปลดล็อค; -ดูการใช้งานนามธรรม:
แพ็คเกจ cc.lixiaohui.lock; นำเข้า java.util.concurrent.timeUnit;/*** การใช้งานโครงกระดูกของล็อคขั้นตอนจริงในการรับล็อคจะถูกนำไปใช้โดย subclasses * * @author lixiaohui * * /บทคัดย่อระดับสาธารณะบทคัดย่อ Abstractlock ใช้ล็อค { /** * <pre> * การมองเห็นจำเป็นต้องรับประกันว่าจะต้องมีการหารือที่นี่หรือไม่เพราะมันเป็นล็อคแบบกระจาย * 1 ต้องรับประกันการมองเห็น * </pre> */ บูลีนผันผวนที่ได้รับการป้องกันล็อค; / ** * เธรดที่ถือล็อคใน JVM (ถ้ามีหนึ่ง) */ เธรดส่วนตัว ExclusiveOwnerThread; ล็อคโมฆะสาธารณะ {ลอง {ล็อค (เท็จ, 0, null, เท็จ); } catch (interruptedException e) {// todo ละเว้น}} โมฆะสาธารณะ public lockinctily โยน interruptedException {lock (false, 0, null, true); } public boolean trylock (นาน, unit timeunit) {ลอง {return lock (จริง, เวลา, หน่วย, เท็จ); } catch (interruptedException e) {// todo ละเว้น} return false; } บูลีนสาธารณะ trylockinctibly (นาน, unit timeunit) พ่น InterruptedException {return lock (จริง, เวลา, หน่วย, true); } โมฆะสาธารณะปลดล็อค {// todo ตรวจสอบว่าเธรดปัจจุบันถือล็อคหรือไม่ (thread.currentthread! = getExClusiveOwnerThread) {โยนใหม่ unlegalMonitonStateException ("เธรดปัจจุบันไม่ถือล็อค"); } ปลดล็อค 0; SetExclusiveOwnerThread (NULL); } void protected setExClusiveOwnerThread (เธรดเธรด) {exclusiveOwnerThread = เธรด; } การป้องกันเธรดสุดท้าย getExClusiveOwnerThread {return exclusiveOwnerThread; } การป้องกันบทคัดย่อโมฆะปลดล็อค 0; / ** * การใช้งานการปิดกั้นล็อคการได้มาซึ่ง * * @param usetimeout * @param เวลา * @param unit * @param ขัดจังหวะว่าจะตอบสนองต่อการขัดจังหวะ * @return * @throws interruptedException */ การป้องกันการล็อคบูลีน ขึ้นอยู่กับการใช้งานขั้นสุดท้ายของ Redis รหัสสำคัญสำหรับการรับและปล่อยล็อคอยู่ในวิธี lock และวิธี unlock0 ของคลาสนี้ คุณสามารถดูวิธีการทั้งสองนี้และเขียนด้วยตัวเองอย่างสมบูรณ์:
แพ็คเกจ cc.lixiaohui.lock; นำเข้า java.util.concurrent.timeUnit; นำเข้า redis.clients.jedis.jedis;/** * <pre> * ล็อคแบบกระจายที่นำมาใช้โดยการดำเนินการ setnx ตาม Redis * * เป็นเวลาที่ดีที่สุด href = "http://redis.io/commands/setnx"> setnc อ้างอิงการดำเนินงาน </a> * </setns @author lixiaohui * */คลาสสาธารณะ redisbaseddistributeDlock ขยายบทคัดย่อ {ส่วนตัว jedis jedis; // ชื่อของล็อคสตริงที่ป้องกันล็อค; // ระยะเวลาความถูกต้องของล็อค (MS) ป้องกัน Lockexpires ยาว; สาธารณะ redisbaseddistributedLock (Jedis Jedis, String Lockkey, Long Lockexpires) {this.jedis = Jedis; this.lockkey = lockkey; this.lockexpires = lockexpires; } // การใช้งานการปิดกั้นล็อคการล็อคบูลีนล็อค (บูลีน useTimeout, นาน, หน่วยเวลา, การขัดจังหวะบูลีน) พ่น InterruptedException {ถ้า (ขัดจังหวะ) {checkInterruption; } Long Start = System.currentTimeMillis; การหมดเวลายาว = unit.tomillis (เวลา); // ถ้า! usetimeout แล้วมันไร้ประโยชน์ในขณะที่ (usetimeout? istimeout (เริ่มต้นหมดเวลา): จริง) {ถ้า (ขัดจังหวะ) {checkInterruption; } long lockexpiretime = system.currentTimeMillis + lockexpires + 1; // ล็อคการหมดเวลาสตริงสตริงสตริง stringoflockexpiretime = string.valueof (lockexpiretime); if (jedis.setnx (Lockkey, StringoflockexpireTime) == 1) {// ที่ได้รับล็อค // todo ได้รับการล็อคสำเร็จตั้งค่าตัวระบุที่เกี่ยวข้องล็อค = true; setExClusiveOwnerThread (thread.currentthread); กลับมาจริง; } ค่าสตริง = jedis.get (lockkey); if (value! = null && istimeExpired (value)) {// ล็อคหมดอายุ // สมมติว่าหลายเธรด (non-single jvm) มาที่นี่ในเวลาเดียวกันสตริง oldValue = jedis.getSet // getset เป็นอะตอม // แต่ oldValue ที่ได้รับจากแต่ละเธรดเมื่อมันมาที่นี่เป็นไปไม่ได้แน่นอนที่จะเหมือนกัน (เพราะ getset เป็นอะตอม) // oldValue ที่ได้รับโดยการเข้าร่วมยังคงหมดอายุแล้วมันหมายความว่าล็อคจะได้รับหากการล็อค setExClusiveOwnerThread (thread.currentthread); กลับมาจริง; }} else {// todo lock ยังไม่หมดอายุให้ป้อนการวนซ้ำถัดไป retrying}} return false; } บูลีนสาธารณะ trylock {long lockexpiretime = system.currentTimeMillis + lockexpires + 1; // ล็อคเวลาหมดเวลาสตริงสตริงสตริงสตริงสตริง stringoflockexpiretime = string.valueof (lockexpiretime); if (jedis.setnx (Lockkey, StringoflockexpireTime) == 1) {// รับล็อค // todo ได้รับการล็อคสำเร็จตั้งค่าตัวระบุที่เกี่ยวข้องล็อค = true; setExClusiveOwnerThread (thread.currentthread); กลับมาจริง; } ค่าสตริง = jedis.get (lockkey); if (value! = null && istimeExpired (value)) {// ล็อคหมดอายุ // สมมติว่าหลายเธรด (ไม่ใช่ jvm เดี่ยว) มาที่นี่ในเวลาเดียวกันกับสตริง oldValue = jedis.getSet // getset เป็นอะตอม // แต่ oldValue ที่ได้รับจากแต่ละเธรดเมื่อมันมาที่นี่เป็นไปไม่ได้แน่นอน (เพราะ getset เป็นอะตอม) // ถ้า oldValue ที่คุณได้รับจะยังคงหมดอายุอยู่นั่นหมายความว่าคุณได้ล็อคถ้า (oldValue! = null && isTimeExpired setExClusiveOwnerThread (thread.currentthread); กลับมาจริง; }} else {// todo lock ยังไม่หมดอายุให้ป้อนลูปถัดไปใหม่ retry} return false; } /*** แบบสอบถามหากล็อคนี้ถือโดยเธรดใด ๆ * * @return {@code true} หากเธรดใด ๆ ถือล็อคนี้และ * {@code false} มิฉะนั้น */ บูลีนสาธารณะ islocked {ถ้า (ล็อค) {return true; } else {ค่าสตริง = jedis.get (lockkey); // สิ่งที่ต้องทำจริง ๆ แล้วมีปัญหาที่นี่ คิดว่า: เมื่อวิธีการรับส่งคืนค่าสมมติว่าค่าหมดอายุแล้ว // ในขณะนี้โหนดอื่นตั้งค่าค่าและล็อคจะถูกเก็บโดยเธรดอื่น (โหนดถือ) และการตัดสินครั้งต่อไป // ไม่สามารถตรวจจับสถานการณ์นี้ได้ อย่างไรก็ตามปัญหานี้ไม่ควรทำให้เกิดปัญหาอื่น ๆ เนื่องจากวัตถุประสงค์ของวิธีนี้คือ // ไม่ใช่การควบคุมแบบซิงโครนัสมันเป็นเพียงรายงานสถานะการล็อค return! istimeExpired (ค่า); }} @Override void void unlock0 {// todo กำหนดว่าล็อคหมดอายุค่าสตริง = jedis.get (lockkey); if (! iTimeExpired (ค่า)) {dounlock; }} โมฆะส่วนตัว checkInterruption พัด interruptedException {if (thread.currentthread.isinterrupted) {โยน InterruptedException ใหม่; }} บูลีนส่วนตัว istimeExpired (ค่าสตริง) {return long.parselong (value) <system.currentTimeMillis; } บูลีนส่วนตัว istimeout (เริ่มต้นยาว, ใช้เวลานาน) {return start + timeout> system.currenttimeMillis; } โมฆะส่วนตัว dounlock {jedis.del (Lockey); - หากคุณเปลี่ยนวิธีการใช้งานในอนาคต (เช่น zookeeper ฯลฯ ) คุณสามารถสืบทอด AbstractLock โดยตรงและใช้ L ock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt) และวิธี unlock0 (เรียกว่านามธรรม)
ทดสอบ
จำลองผู้ปลูก ID ทั่วโลกและออกแบบคลาส IDGenerator คลาสนี้มีหน้าที่สร้าง ID ที่เพิ่มขึ้นทั่วโลก รหัสของมันมีดังนี้:
แพ็คเกจ cc.lixiaohui.lock; นำเข้า java.math.biginteger; นำเข้า java.util.concurrent.timeUnit;/** * จำลองการสร้าง ID * @author lixiaohui * */คลาสสาธารณะ idgenerator ล็อคล็อคสุดท้ายส่วนตัว; การเพิ่มขึ้นของ BigInteger ครั้งสุดท้ายส่วนตัว = BigInteger.ValueOf (1); Idgenerator สาธารณะ (ล็อคล็อค) {this.lock = ล็อค; } สตริงสาธารณะ getandincrement {if (lock.trylock (3, timeUnit.seconds)) {ลอง {// todo รับล็อคที่นี่และเข้าถึงทรัพยากรพื้นที่วิกฤตกลับมา getandincrement0; } ในที่สุด {lock.unlock; }} return null; // กลับ getandincrement0; } สตริงส่วนตัว getandincrement0 {string s = id.toString; id = id.add (เพิ่ม); กลับ s; - การทดสอบตรรกะหลัก: สองเธรดถูกเปิดใน JVM เดียวกันในลูปที่ตายแล้ว (ไม่มีช่วงเวลาระหว่างลูปถ้ามีการทดสอบจะไม่มีความหมาย) เพื่อรับ ID (ฉันไม่ใช่ลูปตาย แต่ทำงานเป็นเวลา 20 ปี) รับ ID และเก็บไว้ใน Set เดียวกัน ก่อนที่จะถูกเก็บไว้ให้ตรวจสอบว่า ID มีอยู่ใน set หรือไม่ หากมีอยู่แล้วปล่อยให้ทั้งสองเธรดหยุด หากโปรแกรมสามารถทำงานได้ 20 วินาทีโดยปกติหมายความว่าล็อคแบบกระจายนี้สามารถตอบสนองความต้องการได้ ผลของการทดสอบดังกล่าวควรเป็นเช่นเดียวกับ JVM ที่แตกต่างกัน (นั่นคือในสภาพแวดล้อมที่กระจายจริง) ต่อไปนี้เป็นรหัสของคลาสทดสอบ:
แพ็คเกจ cc.lixiaohui.distributedlock.distributedlock นำเข้า java.util.hashset; นำเข้า java.util.set; นำเข้า org.junit.test; นำเข้า redis.jedis.jedis cc.lixiaohui.lock.redisbaseddistributeDlock; Idgeneratortest ระดับสาธารณะ {ชุดคงที่ส่วนตัว <String> generatedIds = new HashSet <String>; สตริงสุดท้ายคงที่ส่วนตัว lock_key = "lock.lock"; ส่วนตัวคงที่ LONG_EXPIRE ส่วนตัว = 5 * 1000; @Test การทดสอบโมฆะสาธารณะพัดผ่านการขัดจังหวะการรับรู้ {Jedis Jedis1 = New Jedis ("LocalHost", 6379); Lock Lock1 = ใหม่ redisbasedDistributedLock (Jedis1, lock_key, lock_expire); idgenerator g1 = idgenerator ใหม่ (lock1); IDCONSUMEMISTION ConsUME1 = IDCONSUMESMISTION ใหม่ (G1, "ConsUME1"); Jedis Jedis2 = New Jedis ("Localhost", 6379); Lock Lock2 = ใหม่ redisbasedDistributedLock (Jedis2, lock_key, lock_expire); idgenerator g2 = idgenerator ใหม่ (Lock2); IDCONSUMEMISTION ConsUME2 = IDCONSUMESMISTION ใหม่ (G2, "Consume2"); เธรด t1 = เธรดใหม่ (Consume1); เธรด t2 = เธรดใหม่ (ensume2); t1.start; t2.start; Thread.sleep (20 * 1000); // ให้สองเธรดทำงานเป็นเวลา 20 วินาที idconsumemission.stop; t1.oin; t2.join; } เวลาสตริงคงที่ {return string.valueof (System.currentTimeMillis / 1000); } คลาสคงที่ IDConsumEmission ใช้งาน Runnable {Idgenerator ส่วนตัว idgenerator; ชื่อสตริงส่วนตัว; บูลีนผันผวนแบบคงที่ส่วนตัว Public IdConsumEmission (idgenerator idgenerator, ชื่อสตริง) {this.idgenerator = idgenerator; this.name = ชื่อ; } โมฆะคงที่สาธารณะหยุด {stop = true; } โมฆะสาธารณะเรียกใช้ {system.out.println (เวลา + ": กิน" + ชื่อ + "เริ่ม"); ในขณะที่ (! หยุด) {string id = idgenerator.getandincrement; if (generatedIds.contains (id)) {system.out.println (เวลา + ": ID ซ้ำที่สร้างขึ้น, id =" + id); หยุด = จริง; ดำเนินการต่อ; } generatedIds.add (id); System.out.println (เวลา + ": กิน" + ชื่อ + "เพิ่ม id =" + id); } system.out.println (เวลา + ": กิน" + ชื่อ + "เสร็จสิ้น"); -เพื่อให้ชัดเจนวิธีที่ฉันหยุดสองเธรดที่นี่ไม่ดีมาก ฉันทำสิ่งนี้เพื่อความสะดวกเพราะมันเป็นเพียงการทดสอบดังนั้นจึงเป็นการดีที่สุดที่จะไม่ทำเช่นนี้
ผลการทดสอบ
มีหลายสิ่งที่พิมพ์ออกมาในยุค 20 สิ่งที่พิมพ์อยู่ด้านหน้าจะ clear และใช้ได้เฉพาะเมื่อการวิ่งเกือบจะเสร็จสิ้น ภาพหน้าจอด้านล่าง นี่แสดงให้เห็นว่าล็อคนี้ใช้งานได้ตามปกติ:
เมื่อ IDGererator ไม่ถูกล็อค (นั่นคือวิธี getAndIncrement ของ IDGererator ไม่ได้ล็อคเมื่อได้รับ id ภายใน) การทดสอบจะไม่ผ่านและมีความน่าจะเป็นสูงมากที่จะหยุดอยู่ตรงกลาง ต่อไปนี้เป็นผลการทดสอบเมื่อล็อคไม่ล็อค:
ใช้เวลาน้อยกว่า 1 วินาที:
อันนี้ใช้เวลาน้อยกว่า 1 วินาที:
บทสรุป
ตกลงข้างต้นเป็นเรื่องเกี่ยวกับ Java ที่ใช้ล็อคแบบกระจายตาม Redis หากคุณพบปัญหาใด ๆ คุณหวังว่าจะแก้ไขพวกเขา ฉันหวังว่าบทความนี้จะช่วยคุณศึกษาและทำงานได้ หากคุณมีคำถามใด ๆ คุณสามารถฝากข้อความไว้เพื่อสื่อสาร