Telegram Botda To'lov Integratsiyasi: Idempotentlik Muammosi va Yechimi

Muallif: Javohir Abdullayev · · Telegram botlar

Telegram Botda To'lov Integratsiyasi: Idempotentlik Muammosi va Yechimi

Kirish: Real Loyihadagi Kutilmagan Muammo

Telegram botlar orqali mahsulot yoki xizmat sotish bugungi kunda O'zbekiston bozorida juda ommalashdi. Foydalanuvchi bot ichida buyurtma beradi, Click yoki Payme orqali to'lovni amalga oshiradi va avtomatik ravishda xizmatdan foydalanishni boshlaydi. Biroq, yaqinda ishga tushirilgan elektron chipta sotish botimizda jiddiy muammo yuzaga keldi: ba'zi foydalanuvchilar hisobidan pul bir marta yechilgan bo'lsa-da, bot ularga ikkita bir xil chipta generatsiya qilib berdi, ayrim holatlarda esa foydalanuvchi hisobidan bir necha marta mablag' yechib olindi.

Ushbu maqolada biz ushbu muammoning tub ildizini tahlil qilamiz va backend arxitekturasida idempotentlik (idempotency) tamoyilini qo'llash orqali to'lovlarni qanday qilib 100 foiz xavfsiz va ishonchli qilish mumkinligini real kod misolida ko'rib chiqamiz.

Muammo Nimadan Kelib Chiqdi? (Root Cause Analysis)

Ko'pchilik dasturchilar to'lov tizimlari bilan ishlashda jarayonni juda sodda tasavvur qilishadi: provayderdan webhook keladi, ma'lumotlar bazasida buyurtma holati 'to'landi' deb o'zgartiriladi va foydalanuvchiga xabar jo'natiladi. Ammo real tarmoq sharoitida quyidagi ikkita omil jarayonni buzadi:

  1. Provayderning qayta urinishlari (Retries): Agar sizning serveringiz to'lov tizimi (masalan, Click yoki Payme) so'roviga 2-3 soniya ichida muvaffaqiyatli javob (HTTP 200) qaytara olmasa yoki tarmoqda uzilish bo'lsa, to'lov tizimi serveri aynan bir xil tranzaksiya so'rovini yana qayta yuboradi.
  2. Foydalanuvchi harakatlari: Internet sekin ishlaganda foydalanuvchi to'lov yoki tasdiqlash tugmasini ketma-ket bir necha marta bosishi natijasida parallel so'rovlar yuzaga keladi.

Agar sizning webhook handleringiz bir xil so'rovni ikki marta qabul qilsa va ularni bir vaqtning o'zida parallel qayta ishlasa, poyga holati (race condition) yuzaga keladi. Natijada foydalanuvchiga ikkita xizmat taqdim etiladi yoki tranzaksiya dublikat bo'lib, moliyaviy hisobotlar chalkashib ketadi.

Yechim: Idempotentlik va Taqsimlangan Qulflash (Distributed Lock)

Idempotentlik — bu tizimga bir xil parametrlar bilan necha marta so'rov yuborilishidan qat'i nazar, natija faqat bir marta o'zgarishi va doim bir xil yakuniy holat saqlanishi tamoyilidir. To'lov tizimlarida buni ta'minlash uchun biz ikkita himoya qatlamini joriy qildik:

  • Tranzaksiya holati tekshiruvi: Har bir to'lov so'rovi uchun noyob transaction_id mavjud. Bazada ushbu tranzaksiya avval muvaffaqiyatli bajarilgan bo'lsa, ikkinchi so'rov darhol to'xtatiladi.
  • Redis asosidagi qulf (Distributed Mutex): Parallel kelgan ikki so'rov bir vaqtda tekshiruvdan o'tib ketmasligi uchun Redis yordamida bir necha soniyaga qulf o'rnatiladi.

Amaliy Kod Namunasi: Python va Redis

Quyida FastAPI yoki aiohttp orqali ishlovchi webhook qabul qilgichda Redis yordamida qulf o'rnatish mantig'ini ko'rishingiz mumkin:

import redis
from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
r = redis.Redis(host='localhost', port=6379, db=0)

@app.post('/webhook/payme/')
async def payme_webhook(request: Request):
    data = await request.json()
    transaction_id = data.get('params', {}).get('id')
    order_id = data.get('params', {}).get('account', {}).get('order_id')

    if not transaction_id:
        raise HTTPException(status_code=400, detail='Tranzaksiya ID mavjud emas')

    # Redis orqali 10 soniyaga blokirovka o'rnatamiz (Distributed Lock)
    lock_key = f'lock:payment:{transaction_id}'
    is_acquired = r.set(lock_key, 'locked', nx=True, ex=10)

    if not is_acquired:
        # Agar ushbu tranzaksiya hozirda boshqa oqim tomonidan ishlanayotgan bo'lsa
        return {'error': {'code': -31008, 'message': 'Tranzaksiya ayni paytda bajarilmoqda'}}

    try:
        # 1. Bazadan buyurtmani tekshiramiz
        # 2. Agar buyurtma oldin to'langan bo'lsa, qayta to'lov qilinmaydi
        # 3. Buyurtma holatini yangilaymiz va Telegram orqali xabar yuboramiz
        return {'result': {'state': 2, 'transaction': transaction_id}}
    finally:
        # Ish yakunlangach qulfni bo'shatamiz
        r.delete(lock_key)

Ma'lumotlar Bazasi Darajasida Tranzaksiya Xavfsizligi

Faqat Redis yetarli emas. Agar Redis kesh serveri to'xtab qolsa ham tizim mustahkam ishlashi shart. Buning uchun PostgreSQL kabi relyatsion bazalarda SELECT ... FOR UPDATE buyrug'idan foydalanish zarur. Bu buyurtma qatorini tranzaksiya yakunlanguncha boshqa so'rovlar uchun qulflab turadi.

Django ORM misolida bu quyidagicha amalga oshiriladi:

from django.db import transaction

@transaction.atomic
def process_payment(order_id, transaction_id):
    # select_for_update() boshqa so'rovlarni navbatda ushlab turadi
    order = Order.objects.select_for_update().get(id=order_id)
    
    if order.status == Order.Status.PAID:
        return False, 'Buyurtma allaqachon to\'langan'
        
    order.status = Order.Status.PAID
    order.transaction_id = transaction_id
    order.save()
    
    return True, 'Muvaffaqiyatli yakunlandi'

Xulosa va Real Natijalar

Ushbu arxitektura joriy etilgandan so'ng loyihamizda qanday o'zgarishlar yuz berdi?

  • Takroriy to'lovlar va ortiqcha xizmat ko'rsatish holatlari butunlay nolga tushdi.
  • Server tarmoq uzilishlariga va to'lov provayderlarining agressiv takroriy so'rovlariga to'liq chidamli bo'ldi.
  • Mijozlar tomonidan qo'llab-quvvatlash xizmatiga to'lov bo'yicha tushadigan shikoyatlar bartaraf etildi.

Agar siz Telegram botingizda moliya bilan ishlayotgan bo'lsangiz, har doim idempotentlik qoidalariga rioya qiling. To'lov tizimlari bilan ishlashda 'tezkor yechim' emas, eng avvalo 'xavfsiz va ishonchli arxitektura' muhim o'rinda turadi.

Teglar: #Telegram Bot #To'lov Tizimlari #Python #Redis #Backend