ข้ามไปยังเนื้อหาหลัก
เวอร์ชัน: ล่าสุด

ความเป็นเอกภาพ

ลองคำขอ API ซ้ำอย่างปลอดภัยโดยไม่สร้าง charge, ลูกค้า หรือทรัพยากรอื่นๆ ที่ซ้ำกัน เรียนรู้วิธีการใช้คีย์ความเป็นเอกภาพเพื่อสร้างการผสานการชำระเงินที่เชื่อถือได้

ภาพรวม

ปัญหาเครือข่าย การหมดเวลา และข้อผิดพลาดของเซิร์ฟเวอร์สามารถทำให้คำขอ API ล้มเหลวหรือส่งคืนผลลัพธ์ที่ไม่ชัดเจน ความเป็นเอกภาพช่วยให้คุณสามารถลองคำขอซ้ำอย่างปลอดภัยโดยไม่ต้องกังวลเกี่ยวกับการดำเนินการที่ซ้ำกัน โดยการระบุเฮดเดอร์ Idempotency-Key Omise จะรับประกันว่าคำขอเดียวกันจะให้ผลลัพธ์เดียวกัน แม้ว่าจะถูกส่งหลายครั้งก็ตาม

เริ่มต้นอย่างรวดเร็ว
  • เพิ่มเฮดเดอร์ Idempotency-Key ในคำขอ POST/PATCH
  • ใช้คีย์ที่ไม่ซ้ำกันสำหรับแต่ละการดำเนินการ (แนะนำ UUID)
  • คีย์เดียวกันจะส่งคืนผลลัพธ์เดียวกัน (แคชไว้ 24 ชั่วโมง)
  • จำเป็นสำหรับการสร้าง charge และการดำเนินการเกี่ยวกับเงิน
  • ป้องกันการชำระเงินซ้ำระหว่างปัญหาเครือข่าย

ความเป็นเอกภาพคืออะไร?

ความเป็นเอกภาพ หมายความว่าการดำเนินการหลายครั้งจะให้ผลลัพธ์เดียวกัน ในการประมวลผลการชำระเงิน นี่เป็นสิ่งสำคัญ

ไม่มีความเป็นเอกภาพ

1. ส่งคำขอ charge → หมดเวลาเครือข่าย
2. สำเร็จหรือไม่? ไม่แน่ใจ ลองใหม่หรือไม่?
3. ลองใหม่ → charge ซ้ำ! เรียกเก็บเงินลูกค้าสองครั้ง 💥

มีความเป็นเอกภาพ

1. ส่ง charge พร้อมคีย์ความเป็นเอกภาพ → หมดเวลาเครือข่าย
2. ลองใหม่ด้วยคีย์เดียวกัน → ส่งคืนผลลัพธ์เดียวกัน
3. ไม่มี charge ซ้ำ ✅

ความเป็นเอกภาพทำงานอย่างไร

  1. ส่งคำขอพร้อมเฮดเดอร์ Idempotency-Key
  2. Omise ประมวลผลและบันทึกผลลัพธ์
  3. ลองใหม่ ภายใน 24 ชั่วโมงด้วยคีย์เดียวกัน:
    • Omise จะส่งคืนผลลัพธ์ที่แคชไว้
    • ไม่มีการดำเนินการใหม่
    • ส่งคืนรหัสสถานะและเนื้อหาการตอบกลับเดียวกัน

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

# คำขอแรก (หมดเวลาเครือข่าย)
curl https://api.omise.co/charges \
-X POST \
-u skey_test_...: \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d "amount=100000" \
-d "currency=thb" \
-d "card=tokn_test_..."
# การตอบกลับ: (หมดเวลา - ไม่แน่ใจว่าสำเร็จหรือไม่)

# ลองใหม่ด้วยคีย์เดียวกัน
curl https://api.omise.co/charges \
-X POST \
-u skey_test_...: \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d "amount=100000" \
-d "currency=thb" \
-d "card=tokn_test_..."
# การตอบกลับ: ส่งคืน charge เดิม (ไม่ใช่รายการใหม่)

เมื่อใดควรใช้ความเป็นเอกภาพ

ใช้เสมอสำหรับ:

การสร้าง Charge

POST /charges

สำคัญที่สุด - ป้องกันการชำระเงินซ้ำ

การสร้างลูกค้า

POST /customers

ป้องกันบันทึกลูกค้าที่ซ้ำกัน

การสร้างการคืนเงิน

POST /charges/:id/refunds

ป้องกันการคืนเงินซ้ำ

การสร้างการโอนเงิน

POST /transfers

ป้องกันการชำระเงินซ้ำ

การสร้างผู้รับ

POST /recipients

ป้องกันบันทึกผู้รับที่ซ้ำกัน

คำขอ POST ทั้งหมด ควรใช้คีย์ความเป็นเอกภาพในคำขอ POST ทั้งหมดที่สร้างทรัพยากร

คำขอ PATCH การอัปเดตสามารถลองซ้ำอย่างปลอดภัยด้วยความเป็นเอกภาพ

ไม่จำเป็นสำหรับ:

คำขอ GET การอ่านข้อมูลเป็นเอกภาพอยู่แล้ว (ไม่มีผลข้างเคียง)

คำขอ DELETE การลบเป็นเอกภาพตามธรรมชาติ (ลบสองครั้ง = ผลลัพธ์เดียวกัน)


เฮดเดอร์ Idempotency-Key

รูปแบบเฮดเดอร์

Idempotency-Key: <unique-string>

ข้อกำหนดของคีย์

ข้อกำหนดคำอธิบาย
รูปแบบสตริงใดๆ ที่มีความยาวสูงสุด 255 ตัวอักษร
ความไม่ซ้ำต้องไม่ซ้ำกันสำหรับแต่ละการดำเนินการ
ตัวอักษรแนะนำตัวอักษรและตัวเลขและขีดกลาง
การแยกตัวพิมพ์เล็ก-ใหญ่key-1KEY-1
ระยะเวลาการมีผลเก็บไว้ 24 ชั่วโมง

แนะนำ: ใช้ UUID

# รูปแบบ UUIDv4 (แนะนำ)
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

ทำไมต้อง UUID?

  • ✅ รับประกันความไม่ซ้ำ
  • ✅ ไม่มีความเสี่ยงในการชน
  • ✅ รูปแบบมาตรฐาน
  • ✅ มีในทุกภาษา

ตัวอย่างการใช้งาน

Ruby

require 'omise'
require 'securerandom'

Omise.api_key = ENV['OMISE_SECRET_KEY']

# สร้างคีย์ความเป็นเอกภาพที่ไม่ซ้ำ
idempotency_key = SecureRandom.uuid

begin
charge = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key
})

puts "Charge created: #{charge.id}"

rescue Omise::Error => e
if e.http_status >= 500 || e.message.include?('timeout')
# ลองใหม่อย่างปลอดภัยด้วยคีย์เดียวกัน
charge = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key # คีย์เดียวกัน!
})
else
raise
end
end

Python

import omise
import uuid

omise.api_secret = os.environ['OMISE_SECRET_KEY']

# สร้างคีย์ความเป็นเอกภาพที่ไม่ซ้ำ
idempotency_key = str(uuid.uuid4())

try:
charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
headers={'Idempotency-Key': idempotency_key}
)
print(f"Charge created: {charge.id}")

except omise.errors.BaseError as e:
if e.http_status >= 500:
# ลองใหม่อย่างปลอดภัยด้วยคีย์เดียวกัน
charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
headers={'Idempotency-Key': idempotency_key} # คีย์เดียวกัน!
)
else:
raise

PHP

<?php
require_once 'vendor/autoload.php';

define('OMISE_SECRET_KEY', getenv('OMISE_SECRET_KEY'));

// สร้างคีย์ความเป็นเอกภาพที่ไม่ซ้ำ
$idempotencyKey = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);

try {
$charge = OmiseCharge::create([
'amount' => 100000,
'currency' => 'thb',
'card' => $token
], [
'Idempotency-Key' => $idempotencyKey
]);

echo "Charge created: " . $charge['id'];

} catch (Exception $e) {
if ($e->getCode() >= 500) {
// ลองใหม่ด้วยคีย์เดียวกัน
$charge = OmiseCharge::create([
'amount' => 100000,
'currency' => 'thb',
'card' => $token
], [
'Idempotency-Key' => $idempotencyKey // คีย์เดียวกัน!
]);
} else {
throw $e;
}
}

Node.js

const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const { v4: uuidv4 } = require('uuid');

async function createChargeWithRetry(chargeData, maxRetries = 3) {
// สร้างคีย์ความเป็นเอกภาพที่ไม่ซ้ำ
const idempotencyKey = uuidv4();

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const charge = await omise.charges.create({
...chargeData,
headers: {
'Idempotency-Key': idempotencyKey
}
});

console.log(`Charge created: ${charge.id}`);
return charge;

} catch (error) {
const isServerError = error.statusCode >= 500;
const isTimeout = error.code === 'ETIMEDOUT';
const isLastAttempt = attempt === maxRetries - 1;

if ((isServerError || isTimeout) && !isLastAttempt) {
// ลองใหม่อย่างปลอดภัยด้วยคีย์เดียวกัน
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}

throw error;
}
}
}

// การใช้งาน
createChargeWithRetry({
amount: 100000,
currency: 'thb',
card: 'tokn_test_...'
});

Go

package main

import (
"fmt"
"github.com/google/uuid"
"github.com/omise/omise-go"
"github.com/omise/omise-go/operations"
"time"
)

func createChargeWithRetry(client *omise.Client, amount int64, currency, token string) (*omise.Charge, error) {
// สร้างคีย์ความเป็นเอกภาพที่ไม่ซ้ำ
idempotencyKey := uuid.New().String()

maxRetries := 3

for attempt := 0; attempt < maxRetries; attempt++ {
charge, err := client.CreateCharge(&operations.CreateCharge{
Amount: amount,
Currency: currency,
Card: token,
Headers: map[string]string{
"Idempotency-Key": idempotencyKey,
},
})

if err == nil {
return charge, nil
}

// ตรวจสอบว่าสามารถลองใหม่ได้หรือไม่
if omiseErr, ok := err.(*omise.Error); ok {
if omiseErr.StatusCode >= 500 && attempt < maxRetries-1 {
// ลองใหม่ด้วย exponential backoff
delay := time.Duration(1<<uint(attempt)) * time.Second
time.Sleep(delay)
continue
}
}

return nil, err
}

return nil, fmt.Errorf("max retries exceeded")
}

func main() {
client, _ := omise.NewClient(
os.Getenv("OMISE_PUBLIC_KEY"),
os.Getenv("OMISE_SECRET_KEY"),
)

charge, err := createChargeWithRetry(client, 100000, "thb", "tokn_test_...")
if err != nil {
log.Fatal(err)
}

fmt.Printf("Charge created: %s\n", charge.ID)
}

กลยุทธ์คีย์ความเป็นเอกภาพ

กลยุทธ์ 1: UUID ต่อคำขอ (แนะนำ)

สร้าง UUID ใหม่สำหรับแต่ละการดำเนินการที่ไม่ซ้ำ

# คีย์ที่ไม่ซ้ำสำหรับแต่ละ charge
charge1_key = SecureRandom.uuid
charge1 = create_charge(amount: 100_000, key: charge1_key)

charge2_key = SecureRandom.uuid
charge2 = create_charge(amount: 50_000, key: charge2_key)

ข้อดี:

  • ✅ เรียบง่ายและปลอดภัย
  • ✅ ไม่มีความเสี่ยงในการชน
  • ✅ ทำงานในทุกสถานการณ์

ข้อเสีย:

  • ⚠️ ต้องบันทึกคีย์หากต้องการตรวจสอบสถานะในภายหลัง

กลยุทธ์ 2: สร้างจาก Order ID

สร้างคีย์จาก order/transaction ID ภายใน

def get_idempotency_key(order_id):
return f"order-{order_id}"

# สร้าง charge สำหรับ order
order_id = "ORD-12345"
idempotency_key = get_idempotency_key(order_id)

charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
metadata={'order_id': order_id},
headers={'Idempotency-Key': idempotency_key}
)

ข้อดี:

  • ✅ ง่ายต่อการสร้างคีย์ใหม่ในภายหลัง
  • ✅ เชื่อมโยง charge กับระบบของคุณ
  • ✅ สามารถตรวจสอบว่า order ถูกเรียกเก็บเงินแล้วหรือไม่

ข้อเสีย:

  • ⚠️ ต้องการ order ID ที่ไม่ซ้ำ
  • ⚠️ ไม่สามารถลองใหม่ด้วยพารามิเตอร์ที่ต่างกันได้

กลยุทธ์ 3: บันทึกคีย์พร้อมคำขอ

บันทึกคีย์ในฐานข้อมูล

// บันทึกในฐานข้อมูล
const payment = await db.payments.create({
order_id: 'ORD-12345',
amount: 100000,
idempotency_key: uuidv4(),
status: 'pending'
});

// ใช้คีย์ที่บันทึกไว้สำหรับ charge
const charge = await omise.charges.create({
amount: payment.amount,
currency: 'thb',
card: token,
headers: {
'Idempotency-Key': payment.idempotency_key
}
});

// อัปเดตสถานะ
await db.payments.update(payment.id, {
status: 'completed',
charge_id: charge.id
});

ข้อดี:

  • ✅ สามารถลองใหม่ได้ทุกเมื่อ (คีย์ถูกบันทึกไว้)
  • ✅ ติดตามการตรวจสอบ
  • ✅ สามารถเชื่อมโยงคำขอได้

ข้อเสีย:

  • ⚠️ ซับซ้อนมากขึ้น
  • ⚠️ ต้องการฐานข้อมูล

กลยุทธ์ 4: แฮชพารามิเตอร์คำขอ

สร้างคีย์จากเนื้อหาของคำขอ

<?php
function getIdempotencyKey($chargeParams) {
$content = json_encode($chargeParams);
return hash('sha256', $content);
}

$chargeParams = [
'amount' => 100000,
'currency' => 'thb',
'card' => $token,
'metadata' => ['order_id' => 'ORD-12345']
];

$idempotencyKey = getIdempotencyKey($chargeParams);

$charge = OmiseCharge::create($chargeParams, [
'Idempotency-Key' => $idempotencyKey
]);

ข้อดี:

  • ✅ พารามิเตอร์เดียวกัน = คีย์เดียวกันโดยอัตโนมัติ
  • ✅ ไม่ต้องการที่เก็บข้อมูล
  • ✅ แน่นอน

ข้อเสีย:

  • ⚠️ พารามิเตอร์ต่างกัน = คีย์ต่างกัน (แม้ว่าอาจต้องการ charge เดียวกัน)
  • ⚠️ อ่อนไหวต่อลำดับพารามิเตอร์
  • ⚠️ โทเค็นเปลี่ยนทำให้คีย์เปลี่ยน

การหมดอายุของคีย์

ระยะเวลา 24 ชั่วโมง

คีย์ความเป็นเอกภาพจะถูกเก็บไว้เป็นเวลา 24 ชั่วโมง นับจากการใช้ครั้งแรก

# วันที่ 1, 10:00 น. - คำขอแรก
curl -H "Idempotency-Key: abc123" ...
# สร้าง charge ใหม่และเก็บคีย์ไว้จนถึงวันที่ 2, 10:00 น.

# วันที่ 1, 11:00 น. - ลองใหม่
curl -H "Idempotency-Key: abc123" ...
# ส่งคืนผลลัพธ์ที่แคชไว้จาก 10:00 น.

# วันที่ 2, 11:00 น. - หลังหมดอายุ
curl -H "Idempotency-Key: abc123" ...
# คีย์หมดอายุ สร้าง charge ใหม่ (แตกต่างจากรายการแรก)
การหมดอายุของคีย์

หลังจาก 24 ชั่วโมง คีย์เดียวกันจะสร้างทรัพยากรใหม่ อย่าใช้คีย์ซ้ำสำหรับการดำเนินการที่ต่างกันหรือหลังจาก 24 ชั่วโมง

การจัดการคีย์ที่หมดอายุ

# ❌ ไม่ดี - ใช้คีย์เก่าซ้ำ
def create_charge_for_order(order_id)
# คีย์นี้อาจหมดอายุหาก order เก่า
key = "order-#{order_id}"

Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => key
})
end

# ✅ ดี - ใช้คีย์ใหม่เสมอ
def create_charge_for_order(order_id)
# รวม order ID กับ timestamp
key = "order-#{order_id}-#{Time.now.to_i}"

Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token,
metadata: { order_id: order_id }
}, {
'Idempotency-Key' => key
})
end

แนวทางปฏิบัติที่ดีที่สุดสำหรับตรรกะการลองใหม่

1. ลองใหม่เฉพาะข้อผิดพลาดชั่วคราว

function isRetryable(error) {
// ลองใหม่สำหรับข้อผิดพลาดเซิร์ฟเวอร์
if (error.statusCode >= 500) return true;

// ลองใหม่สำหรับ timeout
if (error.code === 'ETIMEDOUT') return true;
if (error.code === 'ECONNRESET') return true;

// ไม่ลองใหม่สำหรับข้อผิดพลาดไคลเอนต์
if (error.statusCode >= 400 && error.statusCode < 500) return false;

return false;
}

async function chargeWithRetry(chargeData) {
const idempotencyKey = uuidv4();
const maxRetries = 3;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await omise.charges.create({
...chargeData,
headers: { 'Idempotency-Key': idempotencyKey }
});

} catch (error) {
if (!isRetryable(error) || attempt === maxRetries - 1) {
throw error;
}

// exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

2. ใช้ Exponential Backoff

import time
import random

def exponential_backoff(attempt, base_delay=1, max_delay=60):
"""คำนวณความล่าช้าพร้อม jitter"""
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1) # เพิ่ม jitter 10%
return delay + jitter

def create_charge_with_retry(charge_data, max_attempts=5):
idempotency_key = str(uuid.uuid4())

for attempt in range(max_attempts):
try:
return omise.Charge.create(
**charge_data,
headers={'Idempotency-Key': idempotency_key}
)

except omise.errors.BaseError as e:
is_last_attempt = attempt == max_attempts - 1

if e.http_status < 500 or is_last_attempt:
raise

# รอก่อนลองใหม่
delay = exponential_backoff(attempt)
time.sleep(delay)

raise Exception("Max retries exceeded")

3. ตั้งค่า Timeout

require 'timeout'

def create_charge_with_timeout(charge_params, timeout_seconds: 30)
idempotency_key = SecureRandom.uuid

begin
Timeout.timeout(timeout_seconds) do
Omise::Charge.create(
charge_params,
{ 'Idempotency-Key' => idempotency_key }
)
end

rescue Timeout::Error
# timeout - ลองใหม่อย่างปลอดภัยด้วยคีย์เดียวกัน
retry_charge(charge_params, idempotency_key)
end
end

def retry_charge(params, key, max_retries: 3)
max_retries.times do |attempt|
begin
return Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
rescue Omise::Error => e
raise unless e.http_status >= 500
sleep(2 ** attempt)
end
end

raise "Max retries exceeded"
end

4. บันทึกความพยายามในการลองใหม่

const logger = require('./logger');

async function createChargeWithLogging(chargeData) {
const idempotencyKey = uuidv4();
const maxRetries = 3;

logger.info('Creating charge', {
idempotency_key: idempotencyKey,
amount: chargeData.amount,
currency: chargeData.currency
});

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const charge = await omise.charges.create({
...chargeData,
headers: { 'Idempotency-Key': idempotencyKey }
});

logger.info('Charge created successfully', {
idempotency_key: idempotencyKey,
charge_id: charge.id,
attempt: attempt + 1
});

return charge;

} catch (error) {
logger.error('Charge attempt failed', {
idempotency_key: idempotencyKey,
attempt: attempt + 1,
error_code: error.code,
error_message: error.message,
status_code: error.statusCode
});

const isRetryable = error.statusCode >= 500;
const isLastAttempt = attempt === maxRetries - 1;

if (!isRetryable || isLastAttempt) {
logger.error('Charge failed permanently', {
idempotency_key: idempotencyKey,
total_attempts: attempt + 1
});
throw error;
}

const delay = Math.pow(2, attempt) * 1000;
logger.info('Retrying charge', {
idempotency_key: idempotencyKey,
delay_ms: delay,
next_attempt: attempt + 2
});

await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

อ้างอิงอย่างรวดเร็ว

เฮดเดอร์ความเป็นเอกภาพ

Idempotency-Key: <unique-string>

เมื่อใดควรใช้

ประเภทคำขอใช้ความเป็นเอกภาพ?
POST (สร้าง)✅ เสมอ
PATCH (อัปเดต)✅ แนะนำ
GET (อ่าน)❌ ไม่จำเป็น
DELETE❌ ไม่จำเป็น

ระยะเวลาการมีผลของคีย์

  • ระยะเวลาเก็บ: 24 ชั่วโมงนับจากการใช้ครั้งแรก
  • หมดอายุ: สร้างทรัพยากรใหม่หลังจาก 24 ชั่วโมง

ต้นไม้การตัดสินใจการลองใหม่

คำขอล้มเหลว?
├─ ใช่ → ตรวจสอบประเภทข้อผิดพลาด
│ ├─ 5xx ข้อผิดพลาดเซิร์ฟเวอร์ → ลองใหม่ด้วยคีย์เดียวกัน
│ ├─ Network timeout → ลองใหม่ด้วยคีย์เดียวกัน
│ ├─ 4xx ข้อผิดพลาดไคลเอนต์ → แก้ไขคำขอ ใช้คีย์ใหม่
│ └─ ข้อผิดพลาดอื่นๆ → ไม่ลองใหม่
└─ ไม่ → สำเร็จ!

ตัวอย่างการใช้งาน

def create_charge_idempotently(params)
key = SecureRandom.uuid

Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
rescue Omise::Error => e
raise unless e.http_status >= 500

# ลองใหม่ด้วยคีย์เดียวกัน
Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
end

ทรัพยากรที่เกี่ยวข้อง


ถัดไป: เรียนรู้เกี่ยวกับ การกำหนดเวอร์ชัน API เพื่อจัดการการเปลี่ยนแปลงเวอร์ชันและรักษาความเข้ากันได้แบบย้อนหลัง