Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การล็อคในแง่ดีใน Rails REST APIs

ลองนึกภาพสถานการณ์สมมติต่อไปนี้:ในระบบการจัดการทรัพย์สินให้เช่า พนักงาน A เริ่มแก้ไขข้อมูลติดต่อสำหรับการเช่า X โดยเพิ่มหมายเลขโทรศัพท์เพิ่มเติม ในช่วงเวลาเดียวกัน พนักงาน B สังเกตเห็นการสะกดผิดในข้อมูลติดต่อสำหรับ X เช่านั้นและดำเนินการอัปเดต ไม่กี่นาทีต่อมา พนักงาน A จะอัปเดตข้อมูลติดต่อของ Rent X ด้วยหมายเลขโทรศัพท์ใหม่และ ... การอัปเดตการแก้ไขข้อผิดพลาดหายไปแล้ว!

นั่นไม่ดีอย่างแน่นอน! และนี่เป็นสถานการณ์ที่ค่อนข้างเล็กน้อย ลองนึกภาพความขัดแย้งที่คล้ายกันที่เกิดขึ้นในระบบการเงิน!

เราสามารถหลีกเลี่ยงสถานการณ์ดังกล่าวในอนาคตได้หรือไม่? โชคดีคำตอบคือใช่! เราต้องการการป้องกันและการล็อกพร้อมกัน — โดยเฉพาะการล็อกในแง่ดี — เพื่อป้องกันปัญหาดังกล่าว

มาสำรวจการล็อกในแง่ดีใน Rails REST API

'การอัปเดตที่หายไป' และการล็อกในแง่ดีเทียบกับการล็อกในแง่ร้าย

สถานการณ์ที่เราเพิ่งผ่านไปคือประเภท 'การอัปเดตที่หายไป' เมื่อธุรกรรมสองรายการพร้อมกันอัปเดตคอลัมน์เดียวกันของแถวเดียวกัน รายการที่สองจะแทนที่การเปลี่ยนแปลงจากอันแรก เสมือนว่าธุรกรรมแรกไม่เคยเกิดขึ้น

โดยปกติ ปัญหานี้สามารถแก้ไขได้โดย:

  • การตั้งค่าระดับการแยกธุรกรรมที่เหมาะสม ซึ่งจัดการปัญหาในระดับฐานข้อมูล
  • การล็อกในแง่ร้าย — ป้องกันธุรกรรมที่เกิดขึ้นพร้อมกันจากการอัปเดตแถวเดียวกัน ธุรกรรมที่สองจะรอให้ธุรกรรมแรกเสร็จสิ้นก่อนที่จะอ่านข้อมูลด้วยซ้ำ ข้อได้เปรียบที่ดีของที่นี้คือ ใช้งานกับข้อมูลเก่าไม่ได้ ข้อเสียที่สำคัญคือมันยังบล็อกการอ่านข้อมูลจากแถวที่กำหนด
  • Optimistic locking — หยุดการแก้ไขแถวที่กำหนดหากสถานะในขณะที่ทำการแก้ไขแตกต่างจากตอนที่อ่าน

ปัญหาของเราไม่ได้เกี่ยวกับธุรกรรมฐานข้อมูลพร้อมกัน (เหมือนธุรกรรมทางธุรกิจมากกว่า) — ดังนั้นวิธีแรกจึงไม่สามารถใช้งานได้จริง ซึ่งหมายความว่าเราต้องเผชิญกับการล็อคในแง่ร้ายและการล็อคในแง่ดี

การล็อกในแง่ร้ายจะป้องกันไม่ให้การอัปเดตที่สูญหายเกิดขึ้นในสถานการณ์สมมติของเราตั้งแต่แรก อย่างไรก็ตาม มันจะทำให้ชีวิตยากสำหรับผู้ใช้เช่นกัน หากบล็อกการเข้าถึงข้อมูลเป็นเวลานานมาก (ลองนึกภาพว่าอ่านและแก้ไขบางฟิลด์เป็นเวลา 30 นาทีขึ้นไป)

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

มาดูกันว่าเราจะใช้การล็อกในแง่ดีใน Rails REST API สมมุติได้อย่างไร

การล็อกในแง่ดีใน REST API

ก่อนที่เราจะนำไปปรับใช้ในแอพ Rails จริง มาคิดกันก่อนว่าการล็อคในแง่ดีจะเป็นอย่างไรในบริบทของ REST API ทั่วไป

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

สิ่งที่เราต้องหาในบริบทของ REST API คือ:

  • เมื่ออ่านข้อมูลของทรัพยากรที่กำหนด เราจะแสดงสถานะปัจจุบันของวัตถุและส่งคืนเพื่อตอบสนองต่อผู้บริโภคได้อย่างไร
  • ผู้บริโภคควรเผยแพร่สถานะดั้งเดิมของทรัพยากรไปยัง API อย่างไรเมื่อทำการอัปเดต
  • API ควรส่งคืนอะไรให้กับผู้บริโภคหากสถานะมีการเปลี่ยนแปลงและไม่สามารถอัปเดตได้

ข่าวดีก็คือคำถามเหล่านี้สามารถตอบและจัดการกับความหมายของ HTTP ได้

เท่าที่มีการติดตามสถานะของทรัพยากร เราสามารถใช้ประโยชน์จาก Entity Tags (หรือ ETag) เราสามารถส่งคืนลายนิ้วมือ/เช็คซัม/หมายเลขเวอร์ชันของทรัพยากรได้ใน ETag ส่วนหัวไปยังผู้บริโภค API เพื่อส่งในภายหลังด้วยคำขอ PATCH เราสามารถใช้ If-Match ส่วนหัว ทำให้เซิร์ฟเวอร์ API ตรวจสอบว่าทรัพยากรมีการเปลี่ยนแปลงหรือไม่ เป็นเพียงกรณีของการเปรียบเทียบเช็คซัม/หมายเลขเวอร์ชัน/สิ่งอื่นๆ ที่คุณเลือกเป็น ETag

คำขอจะสำเร็จหาก ETag . ปัจจุบัน และ If-Match ค่าจะเหมือนกัน หากไม่เป็นเช่นนั้น API ควรตอบสนองด้วย 412 Precondition Failed . ที่ไม่ค่อยได้ใช้ สถานะ สถานะที่เหมาะสมและแสดงออกมากที่สุดที่เราสามารถใช้เพื่อการนี้ได้

มีอีกสถานการณ์หนึ่งที่เป็นไปได้ เราสามารถเปรียบเทียบ ETags ได้ก็ต่อเมื่อผู้บริโภค API จัดเตรียม If-Match หัวข้อ. เกิดอะไรขึ้นถ้ามันไม่ได้? คุณอาจละเลยการป้องกันภาวะพร้อมกันและลืมการล็อกในแง่ดีไปได้เลย แต่นั่นอาจไม่เหมาะ อีกวิธีหนึ่งคือการกำหนดให้มี If-Match ส่วนหัวและตอบกลับด้วย 428 Precondition Required สถานะถ้าไม่ใช่

ตอนนี้เรามีภาพรวมที่ชัดเจนว่าการล็อกในแง่ดีนั้นสามารถทำงานใน REST API ได้อย่างไร มาใช้งานใน Rails กัน

การล็อกในแง่ดีใน Rails

ข่าวดีก็คือ Rails นำเสนอการล็อคในแง่ดีตั้งแต่แกะกล่อง — เราสามารถใช้คุณสมบัติที่จัดหาให้โดย ActiveRecord::Locking::Optimistic . เมื่อคุณเพิ่ม lock_version คอลัมน์ (หรืออย่างอื่นที่คุณต้องการ แม้ว่าจะต้องมีการประกาศเพิ่มเติมในระดับโมเดลเพื่อกำหนดคอลัมน์การล็อก) ActiveRecord จะเพิ่มขึ้นหลังจากการเปลี่ยนแปลงแต่ละครั้ง และตรวจสอบว่าเวอร์ชันที่กำหนดในปัจจุบันเป็นเวอร์ชันที่คาดไว้หรือไม่ ถ้ามันเก่าแล้ว ActiveRecord::StaleObjectError ข้อยกเว้นจะเพิ่มขึ้นในการพยายามอัปเดต/ทำลาย

วิธีที่ง่ายที่สุดในการจัดการการล็อกในแง่ดีใน API ของเราคือการใช้ค่าจาก lock_version เป็น ETag ลองทำสิ่งนี้เป็นขั้นตอนแรกใน RentalsController สมมุติฐานของเรา :

class RentalsController
  after_action :assign_etag, only: [:show]
 
  def show
    @rental = Rental.find(params[:id])
    respond_with @rental
  end
 
  private
 
  def assign_etag
    response.headers["ETag"] = @rental.lock_version
  end
end

แน่นอนว่านี่เป็นเวอร์ชันที่เรียบง่ายของคอนโทรลเลอร์ เนื่องจากเราสนใจเฉพาะสิ่งที่จำเป็นสำหรับการล็อกในแง่ดีเท่านั้น ไม่ใช่การรับรองความถูกต้อง การอนุญาต หรือแนวคิดอื่นๆ ซึ่งเพียงพอที่จะเปิดเผย ETag ที่เหมาะสมแก่ผู้บริโภค ตอนนี้เรามาดูแล If-Match ส่วนหัวที่ผู้บริโภคสามารถให้ได้:

class RentalsController
  after_action :assign_etag, only: [:show, :update]
 
  def show
    @rental = Rental.find(params[:id])
    respond_with @rental
  end
 
  def update
    @rental = Rental.find(params[:id])
    @rental.update(rental_params)
    respond_with @rental
  end
 
  private
 
  def assign_etag
    response.headers["ETag"] = @rental.lock_version
  end
 
  def rental_params
    params
      .require(:rental)
      .permit(:some, :permitted, :attributes).merge(lock_version: lock_version_from_if_match_header)
  end
 
  def lock_version_from_if_match_header
    request.headers["If-Match"].to_i
  end
end

และนั่นก็เพียงพอแล้วที่จะทำให้การล็อกในแง่ดีเวอร์ชันน้อยที่สุดทำงานได้! แม้ว่าชัดเจนว่า เราไม่ต้องการตอบกลับ 500 คำตอบหากมีข้อขัดแย้งใดๆ เราจะทำ If-Match จำเป็นสำหรับการอัปเดตใด ๆ ด้วย:

class RentalsController
  before_action :ensure_if_match_header_provided, only: [:update]
  after_action :assign_etag, only: [:show, :update]
 
  rescue_from ActiveRecord::StaleObjectError do
    head 412
  end
 
  def show
    @rental = Rental.find(params[:id])
    respond_with @rental
  end
 
  def update
    @rental = Rental.find(params[:id])
    @rental.update(rental_params)
    respond_with @rental
  end
 
  private
 
  def ensure_if_match_header_provided
     request.headers["If-Match"].present? or head 428 and return
  end
 
  def assign_etag
    response.headers["ETag"] = @rental.lock_version
  end
 
  def rental_params
    params
      .require(:rental)
      .permit(:some, :permitted, :attributes)
      .merge(lock_version: lock_version_from_if_match_header)
  end
 
  def lock_version_from_if_match_header
    request.headers["If-Match"].to_i
  end
end

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

สรุป:ความสำคัญของการล็อกในแง่ดีใน Rails API

การป้องกันการทำงานพร้อมกันมักถูกมองข้ามเมื่อออกแบบ REST API ซึ่งอาจนำไปสู่ผลลัพธ์ที่ร้ายแรง

อย่างไรก็ตาม การใช้การล็อกในแง่ดีใน Rails API ค่อนข้างตรงไปตรงมา ดังที่แสดงในบทความนี้ และจะช่วยหลีกเลี่ยงปัญหาร้ายแรงที่อาจเกิดขึ้นได้

ขอให้สนุกกับการเขียนโค้ด!

ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!