זיכרון עסקאות תוכנה בג'אווה באמצעות רב-גוני

1. סקירה כללית

במאמר זה נבחן את ה- רב-גוני ספרייה - שעוזרת לנו ליישם את הרעיון של זיכרון עסקאות תוכנה בג'אווה.

באמצעות קונסטרוקציות מתוך ספרייה זו, אנו יכולים ליצור מנגנון סנכרון במצב משותף - שהוא פיתרון אלגנטי וקריא יותר מההטמעה הסטנדרטית עם ספריית הליבה של Java.

2. תלות של Maven

כדי להתחיל נצטרך להוסיף את ה- ליבה רב-גונית ספרייה לפום שלנו:

 org.multiverse ליבת מולטי-ויבר 0.7.0 

3. ממשק API רב-גוני

נתחיל מכמה מהיסודות.

זיכרון Transactional Memory (STM) הוא מושג המועבר מעולם מסדי הנתונים של SQL - כאשר כל פעולה מבוצעת במסגרת עסקאות העונות על סיפונה חומצה (אטומיות, עקביות, בידוד, עמידות) נכסים. פה, רק אטומיות, עקביות ובידוד מרוצים מכיוון שהמנגנון פועל בזיכרון.

הממשק העיקרי בספריית Multiverse הוא ה- TxnObject - כל אובייקט עסקה צריך ליישם אותו, והספרייה מספקת לנו מספר מחלקות משנה ספציפיות בהן אנו יכולים להשתמש.

כל פעולה שצריכה להיות ממוקמת בקטע קריטי, הנגיש באמצעות שרשור אחד בלבד ומשתמש בכל אובייקט עסקה - צריך להיות עטוף בתוך StmUtils.atomic () שיטה. קטע קריטי הוא מקום של תוכנית שאינה יכולה להיעשות על ידי יותר משרשור אחד בו זמנית, ולכן הגישה אליה צריכה להיות מוגנת על ידי מנגנון סינכרון כלשהו.

אם פעולה במסגרת עסקה תצליח, העסקה תבוצע והמדינה החדשה תהיה נגישה לשרשורים אחרים. אם תתרחש שגיאה כלשהי, העסקה לא תבוצע ולכן המדינה לא תשתנה.

לבסוף, אם שני שרשורים רוצים לשנות את אותה מצב בתוך עסקה, רק אחד יצליח ויעשה את שינוייו. השרשור הבא יוכל לבצע את פעולתו במסגרת העסקה שלו.

4. יישום לוגיקה בחשבון באמצעות STM

בואו נסתכל על דוגמא.

בואו נגיד שאנחנו רוצים ליצור לוגיקה של חשבון בנק באמצעות STM שמספק ה- רב-גוני סִפְרִיָה. שֶׁלָנוּ חֶשְׁבּוֹן לאובייקט יהיה את lastUpadate חותמת זמן שהיא של TxnLong סוג, ואת איזון שדה המאחסן יתרה שוטפת עבור חשבון נתון והוא של TxnInteger סוּג.

ה TxnLong ו TxnInteger הם שיעורים מה רב-גוני. הם חייבים להתבצע במסגרת עסקה. אחרת, ייזרק חריג. עלינו להשתמש ב- StmUtils כדי ליצור מופעים חדשים של אובייקטי העסקה:

חשבון בכיתה ציבורית {פרטי TxnLong lastUpdate פרטי; יתרת TxnInteger פרטית; חשבון ציבורי (יתרת int) {this.lastUpdate = StmUtils.newTxnLong (System.currentTimeMillis ()); this.balance = StmUtils.newTxnInteger (איזון); }}

לאחר מכן, ניצור את להתאים לפי () שיטה - שתגדיל את היתרה בסכום הנתון. פעולה זו צריכה להתבצע בתוך עסקה.

אם יוצא חריג כלשהו בתוכו, העסקה תסתיים מבלי לבצע כל שינוי:

חלל ציבורי adjustBy (סכום int) {adjustBy (סכום, System.currentTimeMillis ()); } public void adjustBy (int כמות, תאריך ארוך) {StmUtils.atomic (() -> {balance.increment (amount); lastUpdate.set (date); if (balance.get () <= 0) {זרוק IllegalArgumentException ("לא מספיק כסף"); } }); }

אם אנו רוצים לקבל את היתרה הנוכחית עבור החשבון הנתון, עלינו לקבל את הערך משדה היתרה, אך יש גם להפעיל אותו באמצעות סמנטיקה אטומית:

getBalance שלם שלם ציבורי () {return balance.atomicGet (); }

5. בדיקת החשבון

בואו נבדוק את שלנו חֶשְׁבּוֹן הִגָיוֹן. ראשית, אנו רוצים להפחית את היתרה מהחשבון בסכום הנתון בפשטות:

@Test הציבור בטל givenAccount_whenDecrement_thenShouldReturnProperValue () {חשבון a = חשבון חדש (10); a.adjustBy (-5); assertThat (a.getBalance ()). isEqualTo (5); }

לאחר מכן, בואו נגיד שאנחנו מושכים מהחשבון והופכים את היתרה לשלילית. פעולה זו צריכה להוציא חריג ולהשאיר את החשבון על כנו משום שהפעולה בוצעה במסגרת עסקה ולא בוצעה:

@Test (צפוי = IllegalArgumentException.class) חלל ציבורי givenAccount_whenDecrementTooMuch_thenShouldThrow () {// נתון חשבון a = חשבון חדש (10); // כאשר a.adjustBy (-11); } 

בואו נבדוק כעת בעיה במקביל שיכולה להיווצר כאשר שני שרשורים רוצים להקטין איזון בו זמנית.

אם חוט אחד רוצה להקטין אותו ב -5 והשני ב 6, אחת משתי הפעולות האלה צריכה להיכשל מכיוון שהיתרה הנוכחית של החשבון הנתון שווה ל -10.

אנו הולכים להגיש שני שרשורים ל- שירות ExecutorService, והשתמש ב- CountDownLatch להתחיל אותם במקביל:

ExecutorService ex = Executors.newFixedThreadPool (2); חשבון א = חשבון חדש (10); CountDownLatch countDownLatch = CountDownLatch חדש (1); ExceptionTrowne AtomicBoolean = AtomicBoolean חדש (שקר); ex.submit (() -> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} try {a.adjustBy (-6);} catch (IllegalArgumentException e) {exceptionTrowne. set (true);}}); ex.submit (() -> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} try {a.adjustBy (-5);} catch (IllegalArgumentException e) {exceptionTrowne. set (true);}});

לאחר בהייה בשתי הפעולות בו זמנית, אחת מהן תזרוק חריג:

countDownLatch.countDown (); ex.awaitTermination (1, TimeUnit.SECONDS); כיבוי לשעבר (); assertTrue (exceptionThrown.get ());

6. העברה מחשבון אחד לאחר

בואו נגיד שאנחנו רוצים להעביר כסף מחשבון אחד לשני. אנחנו יכולים ליישם את העבר ל() שיטה על חֶשְׁבּוֹן בכיתה על ידי העברת השני חֶשְׁבּוֹן שאליו אנו רוצים להעביר את סכום הכסף הנתון:

העברת חלל ציבורית לציבור (חשבון אחר, סכום int) {StmUtils.atomic (() -> {long date = System.currentTimeMillis (); adjustBy (-mount, date); other.adjustBy (amount, date);}); }

כל ההיגיון מתבצע בתוך עסקה. זה יבטיח שכאשר נרצה להעביר סכום גבוה מהיתרה בחשבון הנתון, שני החשבונות יהיו שלמים מכיוון שהעסקה לא תחויב.

בואו נבדוק העברת לוגיקה:

חשבון א = חשבון חדש (10); חשבון b = חשבון חדש (10); a.transferTo (b, 5); assertThat (a.getBalance ()). isEqualTo (5); assertThat (b.getBalance ()). isEqualTo (15);

אנחנו פשוט יוצרים שני חשבונות, אנחנו מעבירים את הכסף אחד לשני, והכל עובד כמצופה. לאחר מכן, בואו נגיד שאנחנו רוצים להעביר יותר כסף ממה שיש בחשבון. ה העבר ל() שיחה תזרוק את אי-חוקי ארגומנט, והשינויים לא יבוצעו:

נסה את {a.transferTo (b, 20); } לתפוס (IllegalArgumentException e) {System.out.println ("נכשל בהעברת כסף"); } assertThat (a.getBalance ()). isEqualTo (5); assertThat (b.getBalance ()). isEqualTo (15);

שים לב שהיתרה לשניהם א ו ב חשבונות זהים לפני השיחה אל העבר ל() שיטה.

7. STM בטוח

כאשר אנו משתמשים במנגנון הסנכרון הסטנדרטי של Java, ההיגיון שלנו יכול להיות נוטה למבוי סתום, ללא דרך להתאושש מהם.

מבוי סתום יכול להתרחש כאשר אנו רוצים להעביר את הכסף מחשבון א לקחת בחשבון ב. ביישום Java רגיל, חוט אחד צריך לנעול את החשבון א, ואז חשבון ב. בואו נגיד שבינתיים החוט השני רוצה להעביר את הכסף מחשבון ב לקחת בחשבון א. החשבון האחר נועל חשבון ב מחכה לחשבון א להיות נעול.

למרבה הצער, הנעילה לחשבון א מוחזק על ידי החוט הראשון, והמנעול לחשבון ב מוחזק על ידי החוט השני. מצב כזה יגרום לחסימת התוכנית שלנו ללא הגבלת זמן.

למרבה המזל, בעת ביצוע העבר ל() היגיון באמצעות STM, איננו צריכים לדאוג למנעולים מכיוון שה- STM בטוח. בואו לבדוק את זה באמצעות שלנו העבר ל() שיטה.

בואו נגיד שיש לנו שני אשכולות. החוט הראשון רוצה להעביר קצת כסף מהחשבון א לקחת בחשבון ב, והחוט השני רוצה להעביר קצת כסף מחשבון ב לקחת בחשבון א. עלינו ליצור שני חשבונות ולהתחיל שני שרשורים שיבצעו את העבר ל() שיטה באותו זמן:

ExecutorService ex = Executors.newFixedThreadPool (2); חשבון א = חשבון חדש (10); חשבון b = חשבון חדש (10); CountDownLatch countDownLatch = CountDownLatch חדש (1); ex.submit (() -> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} a.transferTo (b, 10);}); ex.submit (() -> {try {countDownLatch.await ();} catch (InterruptedException e) {e.printStackTrace ();} b.transferTo (a, 1);});

לאחר תחילת העיבוד, לשני החשבונות יהיה שדה היתרה המתאים:

countDownLatch.countDown (); ex.awaitTermination (1, TimeUnit.SECONDS); כיבוי לשעבר (); assertThat (a.getBalance ()). isEqualTo (1); assertThat (b.getBalance ()). isEqualTo (19);

8. מסקנה

במדריך זה, הסתכלנו על רב-גוני הספרייה וכיצד נוכל להשתמש בה ליצירת לוגיקה ללא נעילה ובטוחה בשרשור תוך שימוש במושגים בזיכרון העיסוק בתוכנה.

בדקנו את התנהגות ההיגיון המיושם וראינו כי ההיגיון המשתמש ב- STM אינו סגור.

ניתן למצוא את היישום של כל הדוגמאות וקטעי הקוד בפרויקט GitHub - זהו פרויקט Maven, כך שיהיה קל לייבא ולהפעיל אותו כפי שהוא.


$config[zx-auto] not found$config[zx-overlay] not found