Solution

การใช้ Idempotent Key ในระบบ Payment เพื่อป้องกัน Race Condition

การใช้ Idempotent Key ในระบบ Payment เพื่อป้องกัน Race Condition

ในการพัฒนาระบบที่ต้องจัดการกับธุรกรรมการเงิน เช่น ระบบ Payment การส่งคำขอซ้ำๆ อาจทำให้เกิดปัญหา Race Condition ได้ ตัวอย่างเช่น ผู้ใช้อาจกดปุ่ม “ชำระเงิน” หลายครั้งในเวลาเดียวกัน ทำให้ระบบได้รับคำขอซ้ำๆ และประมวลผลหลายครั้ง จนทำให้เกิดการเรียกเก็บเงินซ้ำ หรือเกิดปัญหาอื่นๆ ที่ไม่พึงประสงค์

เพื่อแก้ปัญหานี้ เราสามารถใช้แนวคิด Idempotent Key ในการพัฒนาระบบเพื่อให้แน่ใจว่าการประมวลผลคำขอซ้ำจะไม่ก่อให้เกิดผลลัพธ์ที่ต่างจากการประมวลผลเพียงครั้งเดียว

Idempotent Key คืออะไร?

Idempotent Key คือคีย์ที่ใช้ระบุคำขอ (request) ที่ไม่ซ้ำกัน หากระบบได้รับคำขอซ้ำโดยมี Idempotent Key เดียวกัน ระบบจะสามารถตรวจสอบและระบุได้ว่าคำขอนี้ถูกประมวลผลไปแล้วหรือยัง ถ้าประมวลผลไปแล้ว ระบบจะไม่ดำเนินการซ้ำอีก

ตัวอย่างการนำ Idempotent Key ไปใช้งานในระบบ Payment ตัวอย่างนี้จะเป็นการใช้ Golang โดยมีการใช้สถาปัตยกรรมแบบ Clean Architecture และ RabbitMQ ในการจัดการคิว นอกจากนี้ยังใช้ฐานข้อมูล SQL ในการจัดเก็บข้อมูล

Step 1: สร้าง Idempotent Key สำหรับแต่ละคำขอ ในกรณีนี้ เราสามารถใช้ UUID หรือ Token ที่ไม่ซ้ำกันเป็น Idempotent Key เพื่อระบุคำขอแต่ละคำขอ

package payment

import (
	"database/sql"
	"errors"
	"github.com/google/uuid"
)

type PaymentRequest struct {
	UserID        string
	Amount        float64
	IdempotentKey string
}

type PaymentService struct {
	DB *sql.DB
}

func (s *PaymentService) ProcessPayment(request PaymentRequest) error {
	// ตรวจสอบว่า Idempotent Key ถูกใช้งานไปหรือยัง
	var existingPaymentID string
	err := s.DB.QueryRow("SELECT id FROM payments WHERE idempotent_key = ?", request.IdempotentKey).Scan(&existingPaymentID)
	if err != nil && err != sql.ErrNoRows {
		return err
	}

	// ถ้า Idempotent Key มีอยู่แล้ว แสดงว่าคำขอถูกประมวลผลแล้ว
	if existingPaymentID != "" {
		return errors.New("payment already processed")
	}

	// เริ่มการประมวลผลธุรกรรม
	tx, err := s.DB.Begin()
	if err != nil {
		return err
	}

	_, err = tx.Exec("INSERT INTO payments (user_id, amount, idempotent_key) VALUES (?, ?, ?)", request.UserID, request.Amount, request.IdempotentKey)
	if err != nil {
		tx.Rollback()
		return err
	}

	// ยืนยันการทำธุรกรรม
	err = tx.Commit()
	if err != nil {
		return err
	}

	return nil
}

Step 2: การทำงานร่วมกับ RabbitMQ เพื่อประมวลผลคำขอแบบ Asynchronous การใช้ RabbitMQ ช่วยในการประมวลผลคำขอแบบ Asynchronous ทำให้ระบบสามารถจัดการกับคำขอที่มีจำนวนมากได้อย่างมีประสิทธิภาพ และยังสามารถป้องกันปัญหา Race Condition ได้อีกด้วย

package main

import (
	"encoding/json"
	"log"
	"payment"
	"github.com/streadway/amqp"
)

func main() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("Failed to connect to RabbitMQ: %s", err)
	}
	defer conn.Close()

	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("Failed to open a channel: %s", err)
	}
	defer ch.Close()

	q, err := ch.QueueDeclare(
		"payment_queue",
		false,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		log.Fatalf("Failed to declare a queue: %s", err)
	}

	msgs, err := ch.Consume(
		q.Name,
		"",
		true,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		log.Fatalf("Failed to register a consumer: %s", err)
	}

	paymentService := payment.PaymentService{} // เพิ่มการเชื่อมต่อกับฐานข้อมูลที่นี่

	forever := make(chan bool)

	go func() {
		for d := range msgs {
			var req payment.PaymentRequest
			err := json.Unmarshal(d.Body, &req)
			if err != nil {
				log.Printf("Error decoding JSON: %s", err)
				continue
			}

			err = paymentService.ProcessPayment(req)
			if err != nil {
				log.Printf("Error processing payment: %s", err)
			}
		}
	}()

	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	<-forever
}

การทำงานของระบบ เมื่อผู้ใช้ส่งคำขอชำระเงิน ระบบจะสร้าง Idempotent Key สำหรับคำขอนั้นๆ และบันทึกคำขอไปที่ฐานข้อมูล คำขอจะถูกส่งต่อไปที่ RabbitMQ เพื่อประมวลผลแบบ Asynchronous เมื่อ RabbitMQ ได้รับคำขอ จะทำการเรียกใช้ PaymentService เพื่อประมวลผลธุรกรรม โดยมีการตรวจสอบ Idempotent Key ว่าถูกประมวลผลไปแล้วหรือยัง ถ้าคำขอนั้นถูกประมวลผลไปแล้ว ระบบจะหยุดการทำงาน แต่ถ้ายังไม่ถูกประมวลผล ระบบจะดำเนินการชำระเงินต่อไป สรุป การใช้ Idempotent Key ช่วยให้การพัฒนาระบบ Payment มีความปลอดภัยและมีประสิทธิภาพมากขึ้น สามารถป้องกันปัญหา Race Condition ได้อย่างมีประสิทธิผล นอกจากนี้การผสมผสานการใช้ RabbitMQ ยังช่วยเพิ่มความสามารถในการประมวลผลคำขอได้ในปริมาณมากโดยไม่ทำให้ระบบทำงานช้าลง

การนำแนวคิดนี้ไปใช้ในระบบของคุณ จะช่วยให้ธุรกรรมการเงินมีความปลอดภัยมากขึ้น ลดปัญหาการเรียกเก็บเงินซ้ำ และเพิ่มความมั่นใจให้กับผู้ใช้งานได้มากขึ้น