Spring - ใช้ Shedlock แบบ Manual เพื่อใช้ในการทำ Lock

Spring - ใช้ Shedlock แบบ Manual เพื่อใช้ในการทำ Lock

ในยุคที่การเขียน Application เน้นหนักไปที่การทำให้สามารถ Scale out เพื่อรองรับ Load หรือแบ่งงานกันทำ ซึ่งนั่นแปลว่า Application ที่เราเขียนนั้นจะมีหลายตัวทำงานพร้อมๆกัน ทุกอย่างฟังดู OK ไม่มีปัญหา แต่หาก Application ของท่านมีงานสักงานที่ต้องการให้มีแค่ 1 instance (thread , process บลาๆ) ทำงานเท่านั้น

ตัวอย่างงานที่ต้องการแค่คนเดียวทำ

  1. งานที่กำหนดเวลาที่จะเริ่มทำงานได้

    งานพวกนี้เป็นงานที่ตั้งเวลาทำงาน ทำเป็นรอบๆ ส่วนใหญ่จะเป็นงานเกี่ยวกับการหาค่าสรุป หรือ ลบข้อมูล แก้ข้อมูลบางอย่าง ณ เวลานั้น เช่น

    • ทุกสิ้นเดือนจะทำการอ่านข้อมูลจาก database เพื่อออกเป็นรายงาน

    • ทุกวันขึ้นปีใหม่จะทำการแก้เลข Running เป็น 0 ใหม่

    • ทุกสิ้นเดือนทำการลบข้อมูลที่เก่ากว่า 3 เดือนทิ้งไป

      จะเห็นว่างานพวกนี้ต้องการ Run ด้วยคนเดียว (instance, thread , process) ไม่ต้องการให้มีหลายคนมาทำพร้อมกัน ซึ่งถ้าทำพร้อมกันก็อาจเกิดปัญหา เช่น ทำงานซ้ำซ้อน ได้ report 3 ใบให้งงเล่น หรือ แย่งกันลบข้อมูลทำให้เกิด error ว่ามีคนกำลังแย่งกันใช้งาน

  2. งานที่ไม่สามารถกำหนดเวลาที่จะเริ่มทำงานได้

    งานพวกนี้ไม่สามารถกำหนดเวลาได้อย่างแน่นอน อาจจะเกิดจากลูกค้ามากดใช้งานหน้าเว็บ หรือ Application ที่เกี่ยวข้องสั่งให้เริ่มดำเนินงาน ตัวอย่างเช่น

    • ยิงไป Get Token จาก API Third party เมื่อมีลูกค้าเริ่มเข้ามาใช้งาน
      กรณีนี้คือเมื่อมีคนมาใช้งานเป็นการ Trigger ให้ Application เราเริ่มทำงาน แต่ Application เราต้องติดต่อ Third party ซึ่งต้องยิงไปขอ Token เพราะ Token ที่เก็บไว้หมดอายุ เราจึงต้องยิงไปขอ token เพื่อมาเก็บใหม่ ซึ่งถ้ามีลูกค้ามาใช้งานพร้อมกัน 50 คน มันจะกลายเป็น ยิงไปหา Third party 50 ครั้ง แล้วถ้า Third party token เป็นแบบ login แล้ว token เก่าจะใช้ไม่ได้ แปลว่าการยิง 49 ครั้งจะไม่มีความหมาย ดังนั้นเราต้องการแค่ คนเดียวเท่านั้นไปดึง token กลับมาแล้ว save ที่ระบบ

วิธีแก้ปัญหา

ถ้าพูดถึงวิธีแก้ปัญหาเท่าที่คิดแบบง่ายๆในตอนนี้ก็มีหลายวิธีครับ เป็นวิธีบ้านๆเลย

  1. แยกส่วนที่ทำงานแบบนั้นออกเป็นอีก Application เลย

    วิธีนี้ออกจะกำปั้นทุบดินไปหน่อยแต่ก็เป็นวิธีที่ง่าย ไม่ต้องหาวิธีจัดการ แยกมันออกมาแล้วมันเป็นตัวเดียวที่ Run แค่ Instance เดียว วิธีนี้เหมาะกับงานพวก Schedule Task แต่ถ้าเป็นงานอื่นที่ต้องเรียกในเวลาไหนก็ได้ แล้วอยากให้ทำงานแค่ตัวเดียว วิธีนี้แก้ไม่ได้

  2. สร้างคนกลางขึ้นมาเพื่อใช้คุยกันตกลงว่าใครจะได้งาน

    วิธีนี้คือสร้างตัวกลางขึ้นมาเพื่อเก็บข้อมูลตรงกลางแล้วให้หลายๆ Instance เข้ามาใช้ข้อมูลตรงกลางซึ่งมี Application พวกนี้เกิดขึ้นมาขึ้นบนโลกนี้แล้วมากมายไม่ว่าจะเป็น Consul , Apache ZooKeeper และ Relational Database เช่น Mysql postgres โดยการใช้วิธีนี้อาจจะใช้ Lib ที่ติดต่อกับ Applicaion ที่กล่าวมาเพื่อทำบางอย่างเพื่อให้ได้งานมา เช่น

    • หาว่าใครเป็นหัวหน้าแล้วคนที่เป็นหัวหน้าจะได้งานนั้นไป
      วิธีนี้เหมาะกับงานที่เป็น Task เพราะหัวหน้าจะได้งานไปทำ แต่ก็จะติดปัญหาเดิมว่าไม่สามารถใช้กับงานที่ Run เมื่อไหร่ก็ได้
    • ใช้ Lock
      วิธีนี้คือการยิงไปที่ตัวกลางเพื่อขอทำงาน ถ้าไม่มีใครทำงานจะทำการ Lock ไม่ให้มาทำงาน พอตัวเองทำงานเสร็จก็มาปลด Lock เพื่อให้คนอื่นสามารถมาขอทำงานได้บ้าง ด้วยวิธีนี้จะยืดหยุ่นสามารถใช้ได้กับทั้ง Task และงานที่จะทำเมื่อไหร่ก็ได้

ดังนั้นด้วยความยืดหยุ่นและไม่ต้องแยก Application ออกมา ดังนั้นเราจะมาลองใช้วิธี สร้างคนกลางขึ้นมาแล้วให้ Application แต่ละ Instance เข้าไปขอ Lock เอง

ไม่ต้องเขียนเอง Shedlock ทำให้แล้ว

มาถึงจุดนี้หลายคนอาจกังวลว่ามันจะต้องมาเขียนเองหรืออะไรรึเปล่า ก็บอกตรงนี้เลยว่าไม่ต้องเขียนครับ มีคนเขียนให้แล้วเราไปใช้ของเขาดีกว่าครับ ซึ่ง Lib ที่ผมแนะนำคือ Shedlock ซึ่งตัว Shedlock นั้นสามารถใช้ได้กับ Database ทั่วไป , Consul, ZooKeeper, etc, และอื่นๆอีกมากมายสามารถอ่านได้ที่ Github เลย

ใช้ Shedlock กับ Schedule Task

อันนี้ไม่ยากเลยครับ สามารถทำตาม Manual ใน README ได้เลย Scheduled locking (Spring)

ใช้ Shedlock แบบ Manual

จากที่บอกไปงานบางงานมันไม่ได้ Run แบบ Schedule ดังนั้นเราจึงไม่สามารถใช้วิธีใส่ Annotation แบบ Schedule Task ได้ ดังนั้นเราจึงต้องมาทำแบบ Manual โดยผมเขียนตัวอย่างไว้แล้วที่

1
https://github.com/surapong-taotiamton/shedlock-example

สามารถ Clone ลงมาทดสอบได้เลย โดย Code นั้นเขียนโดยใช้ Framework spring แต่ไม่ใช่ Spring ก็สามารถใช้งานได้นะครับ ขอแค่สร้าง LockProvider ให้ได้ก็สามารถใช้งานได้แล้ว

Add lib เข้าไปใน pom.xml

เพิ่ม Dependency เข้าไปใน pom.xml โดยในงานนี้ผมขอใช้ Relational Database เป็นคนกลางในการจัดการ Lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Shedlock -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.20.1</version>
</dependency>

<!--
ใช้ Relational database เลย add dependency นี้ ถ้าใช้ตัวอื่นลองไปหาดูว่าต้อง import อะไร
-->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.20.1</version>
</dependency>

สร้าง Table ให้กับ Shedlock

เนื่องจากเราใช้ Database จึงต้องสร้าง Table ให้ shedlock ใช้ในการจัดการ Lock

1
2
3
4
5
6
7
CREATE TABLE shedlock(
name VARCHAR(64),
lock_until TIMESTAMP(3) NULL,
locked_at TIMESTAMP(3) NULL,
locked_by VARCHAR(255),
PRIMARY KEY (name)
);

สร้าง Bean Lock provider เพื่อใช้ดึง Lock

ส่วนนี้คือการสร้าง Bean Lock provider เพื่อให้สามารถ Inject ไปใช้ที่อื่นได้

Full Code:ShedlockConfiguration.java

1
2
3
4
5
6
7
@Configuration
public class ShedlockConfiguration {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
}

ใช้งาน LockProvider

ในส่วนนี้ขอเป็นการจำลองงานที่เกิดแบบไม่เป็นเวลา แต่อยากให้งานมัน Run แค่ 1 งานในระยะเวลานึง ถ้ามีมาพร้อมกันตัวนึงจะได้ทำงานอีกตัวจะต้องถูกปฏิเสธกลับไป ตรงนี้ผมจะจำลองให้งานเกิดจากการรับ http request เข้ามา จากนั้นให้มันไปแย่ง Lock กัน

Full Code : TestLockController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@RestController
public class TestLockController {

private static final Logger logger = LoggerFactory.getLogger(TestLockController.class);

@Autowired
private LockProvider lockProvider;

@GetMapping("/test-lock")
public String testLock() throws Exception {

// จะตั้งชื่อ LOCK นั้นว่าอะไร ถ้าเป็นงานเดียวกันควรตั้งชื่อเดียวกัน
// เพราะ shedlock แยก lock ด้วยชื่อ
final String LOCK_NAME = "TEST-LOCK-NAME";


// Lock นี้จะ Lock นานสุดเท่าไหร่ อันนี้มีไว้กันเวลาไม่คืน Lock
// พอเกินเวลาที่กำหนดจะได้มีคนเข้าไปขอ Lock ใหม่ได้
Duration lockAtMost = Duration.ofSeconds(60);

// Lock นี้จะ Lock สั้นสุดเท่าไหร่ อันนี้อาจจะงง แต่ไม่งงครับ อันนี้ตั้งไว้ว่า
// หลังจากขอ Lock ไปแล้วจะไม่ให้มีใครมาขอ Lock ได้อีกนานเท่าไหร่
// ต่อให้ทำเสร็จก่อนก็ต้องรอจนกว่าจะถึงเวลาที่กำหนดถึงจะมาขอใหม่ได้
Duration lockAtLeast = Duration.ZERO;

// สร้าง LockConfiguration
LockConfiguration lockConfiguration = new LockConfiguration(
Instant.now(), LOCK_NAME, lockAtMost, lockAtLeast );

logger.info("Begin get lock");
// ขอ Lock
SimpleLock lock = lockProvider.lock(lockConfiguration).orElse(null);
logger.info("End get lock");


if (lock == null) {
// ถ้าไม่ได้ lock lock จะเป็นค่า null code ของเราต้องจัดการว่าถ้าไม่ได้ Lock
// จะทำยังไง ในตัวอย่างนี้ให้ Return กลับไปว่าไม่ได้ Lock
logger.info("Can not get lock");
return "Case : can not get lock";
} else {

// กรณีนี้คือได้ lock เมื่อได้ lock ก็ทำงานอะไรก็ได้ตามใจของเรา
// โดยของผมคือให้มัน Thread sleep ไป 10 วินาทีเพื่อจำลองว่าทำงานอะไรนาน
// ข้อสำคัญคือเมื่อทำงานเสร็จต้องคืน lock เสมอ ไม่ว่าจะสำเร็จไม่สำเร็จ
// จะเห็นว่าใน finally จะสั่ง unlock

logger.info("Get lock and begin do task");
try {
Thread.sleep(10L * 1000L);
logger.info("Do task complete");
} finally {
// อย่าลืมว่าต้อง unlock เสมอไม่ว่าจะสำเร็จหรือไม่สำเร็จ
lock.unlock();
logger.info("unlock complete");
}
return "Case : task complete";
}
}

}

ตัวอย่างการทำงาน

อันนี้ผมสร้าง Post man ยิงไปที่ Server 2 ตัวในเวลาไล่เลี่ยกัน โดยตัวแรกยิงไปก่อนผลลัพธ์จะได้เป็นทำงานสำเร็จ (รอประมาณ 10 วิ ตาม Code ที่ Sleep ไป 10 วิ)

ส่วนตัวที่ยิงไปทีหลังจะได้เป็นไม่สามารถดึง Lock มาใช้งานได้

คราวนี้เราลองมาสำรวจที่ Database ว่ามันทำงานยังไง หลังจากที่เรายิงเข้าเพื่อใช้งาน Lock ตัว shedlock จะเก็บข้อมูลที่ Database ดังนี้

จะเห็นว่าจะมี Lock name ที่ชื่อ TEST-LOCK-NAME โผล่ขึ้นมา โดยจะเห็นว่า locked_at ห่างกับ locked_until เป็นเวลา 60 วินาที ตามที่เราตั้งใน lockAtMost เลย นั่นแปลว่า lock จากนั้นเมื่อทำงานเสร็จ เรามาดู database อีกครั้ง

จะเห็นว่า locked_until จะมีค่าที่เปลี่ยนไป โดยหาสังเกตดีๆคือ 10 วินาทีหลังจาก locked_at โดยตรงนี้ทำให้เราเห็นว่าหลังจากงานของเราทำเสร็จ (ผมสั่ง sleep ไป 10 วินาที) ตัว shedlock ก็ปรับค่า locked_until เป็นเวลาปัจจุบันทันที เนื่องจากเรา set lockAtLeast เป็น 0 วินาที ดังนั้นหากอยากลอง lockAtLeast ว่ามีผลอย่างไรลองไปเปลี่ยนเล่นดูนะครับ เพื่อความเข้าใจที่มากขึ้น

สรุป

สำหรับตอนนี้เราได้รู้ว่าวิธีการใช้ Shedlock แบบ Manual ซึ่งด้วยความรู้เท่านี้ก็สามารถนำไปประยุกต์ใช้กับงานได้มากมาย เมื่อใดมีปัญหาอยากให้คนเดียวทำก็สามารถเอาความรู้ตรงไปประยุกต์ใช้ได้เลย สำหรับตอนต่อไปยังไม่รู้จะเขียนอะไร เอาเป็นว่าถ้างานที่ทำต้องเจออะไรแปลกๆแบบที่ไม่มีใน Internet จะมาเขียนใหม่ให้อ่านกันครับ

โปรโมท Page

ผมทำ Page บน Facebook แล้วนะครับ ใครสนใจรับการแจ้งเตือนเวลาผมอัพเดท Blog ก็ไปกดติดตามกันได้ครับ กดตามลิ้งไปได้เลย Facebook page

ไม่เกี่ยวกับ Code แต่เกี่ยวกับ Idol

น้องมุกวง Wisdom เป็นน้องที่คุยด้วยสนุกมาก เล่นมุกตลอด น่ารัก ลองไปติดตามน้องกันได้

Mook

เพลงประกอบการเขียน Code

เจอผู้หญิงที่ชอบอยากจะจีบแต่ก็เจอว่าเขามีคนคุยอยู่ด้วยแล้ว จะไปแข่งก็ใช่เรื่องก็เลยก็เลย…. ฮ่าๆๆๆๆ