การล็อกแถว InnoDB - วิธีการนำไปใช้


13

ตอนนี้ฉันได้ดูรอบ ๆ แล้วอ่านไซต์ mysql และฉันก็ยังไม่เห็นว่ามันทำงานอย่างไร

ฉันต้องการเลือกและล็อกแถวผลลัพธ์สำหรับการเขียนเขียนการเปลี่ยนแปลงและปลดล็อค เปิดใช้งาน audocommit

โครงการ

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 

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

ดังนั้น;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"

รับ ID จากผลลัพธ์

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>

ฉันต้องทำอะไรเพื่อปลดล็อคและทำงานเหมือนที่ฉันเคยทำด้านบนหรือไม่?

คำตอบ:


26

สิ่งที่คุณต้องการคือเลือก ... สำหรับการอัพเดทจากภายในบริบทของการทำธุรกรรม SELECT FOR UPDATE จะทำการล็อคแบบเอกสิทธิ์เฉพาะบุคคลในแถวที่เลือกไว้ราวกับว่าคุณกำลังดำเนินการ UPDATE นอกจากนี้ยังมีการทำงานในระดับการแยกแบบ READ COMMITTED โดยปริยาย เพิ่งทราบว่า SELECT ... for UPDATE นั้นแย่มากสำหรับการเกิดพร้อมกันและควรใช้เมื่อจำเป็นจริงๆเท่านั้น นอกจากนี้ยังมีแนวโน้มที่จะทวีคูณใน codebase เมื่อผู้คนตัดและวาง

นี่คือตัวอย่างเซสชันจากฐานข้อมูล Sakila ซึ่งแสดงให้เห็นถึงพฤติกรรมบางอย่างของคำสั่ง FOR UPDATE

ก่อนอื่นเราก็เลยต้องชัดเจนตั้งระดับการแยกธุรกรรมเป็น REPEATABLE READ โดยทั่วไปไม่จำเป็นเนื่องจากเป็นระดับการแยกเริ่มต้นสำหรับ InnoDB:

session1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
session1> BEGIN;
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)    

ในเซสชันอื่นอัปเดตแถวนี้ ลินดาแต่งงานแล้วเปลี่ยนชื่อ:

session2> UPDATE customer SET last_name = 'BROWN' WHERE customer_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

ย้อนกลับไปในเซสชั่น 1 เพราะเราอยู่ใน REPEATABLE READ ลินดายังคงเป็นวิลเลียมส์ LINDA:

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)

แต่ตอนนี้เราต้องการสิทธิพิเศษในการเข้าถึงแถวนี้ดังนั้นเราจึงขอให้มีการอัปเดตแถว โปรดสังเกตว่าขณะนี้เราได้รับแถวย้อนหลังล่าสุดซึ่งได้รับการปรับปรุงในเซสชัน 2 นอกธุรกรรมนี้ นั่นไม่ใช่ REPEATABLE READ นั่นคือ READ COMMITTED

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3 FOR UPDATE;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | BROWN     |
+------------+-----------+
1 row in set (0.00 sec)

ลองทดสอบชุดล็อคใน session1 โปรดทราบว่า session2 ไม่สามารถอัปเดตแถว

session2> UPDATE customer SET last_name = 'SMITH' WHERE customer_id = 3;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

แต่เรายังคงสามารถเลือกได้

session2> SELECT c.customer_id, c.first_name, c.last_name, a.address_id, a.address FROM customer c JOIN address a USING (address_id) WHERE c.customer_id = 3;
+-------------+------------+-----------+------------+-------------------+
| customer_id | first_name | last_name | address_id | address           |
+-------------+------------+-----------+------------+-------------------+
|           3 | LINDA      | BROWN     |          7 | 692 Joliet Street |
+-------------+------------+-----------+------------+-------------------+
1 row in set (0.00 sec)

และเรายังสามารถอัพเดทตารางลูกที่มีความสัมพันธ์กับคีย์ต่างประเทศ

session2> UPDATE address SET address = '5 Main Street' WHERE address_id = 7;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1> COMMIT;

ผลข้างเคียงอีกอย่างหนึ่งคือคุณเพิ่มโอกาสในการก่อให้เกิดการหยุดชะงักอย่างมาก

ในกรณีเฉพาะของคุณคุณอาจต้องการ:

BEGIN;
SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;
-- do some other stuff
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>;
COMMIT;

หากชิ้นส่วน "ทำอะไรบางอย่าง" นั้นไม่จำเป็นและคุณไม่จำเป็นต้องเก็บข้อมูลเกี่ยวกับแถวนั้น ๆ SELECT FOR UPDATE นั้นไม่จำเป็นและสิ้นเปลืองและคุณสามารถทำการอัปเดตแทน:

UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;

หวังว่ามันจะสมเหตุสมผล


3
ขอบคุณ ดูเหมือนจะไม่สามารถแก้ปัญหาของฉันได้เมื่อมีสองกระทู้เข้ามาพร้อมกับ "SELECT id FROM itemsWHERE status= 'รอดำเนินการ' LIMIT 1 สำหรับการอัพเดท" และพวกเขาทั้งสองเห็นแถวเดียวกันจากนั้นหนึ่งคนจะล็อคอีกคน ฉันหวังว่าอย่างใดมันจะสามารถบายพาสแถวล็อคและกลับไปรายการถัดไปซึ่งอยู่ระหว่าง ..
Wizzard

1
ธรรมชาติของฐานข้อมูลคือพวกเขาส่งคืนข้อมูลที่สอดคล้องกัน หากคุณดำเนินการแบบสอบถามนั้นสองครั้งก่อนที่ค่าจะได้รับการปรับปรุงคุณจะได้รับผลลัพธ์เดียวกัน ไม่มี "รับฉันค่าแรกที่ตรงกับแบบสอบถามนี้เว้นแต่นามสกุลถูกล็อค" SQL นามสกุลที่ฉันรู้ สิ่งนี้ฟังดูน่าสงสัยว่าคุณกำลังใช้คิวบนฐานข้อมูลเชิงสัมพันธ์ เป็นอย่างนั้นเหรอ?
Aaron Brown

อาโรน ใช่นั่นคือสิ่งที่ฉันพยายามจะทำ ฉันดูที่การใช้สิ่งที่เหมือนเกียร์ - แต่นั่นก็เป็นหน้าอก คุณมีอย่างอื่นในใจ?
Wizzard

ฉันคิดว่าคุณควรอ่านสิ่งนี้: engineyard.com/blog/2011/… - สำหรับคิวข้อความมีจำนวนมากอยู่ที่นั่นขึ้นอยู่กับภาษาของลูกค้าที่คุณเลือก ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ, และอื่น ๆ
Aaron Brown

ฉันจะทำอย่างไรเพื่อให้เซสชั่น 2 บล็อกการอ่านจนกว่าการอัพเดตในเซสชั่น 1 จะถูกส่งไป?
CMCDragonkai

2

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

ไคลเอนต์ที่แตกต่างกันสามารถอ่านแถวเดียวกันพร้อมกัน

ไคลเอนต์ที่แตกต่างกันสามารถปรับเปลี่ยนแถวที่แตกต่างกันพร้อมกัน

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

โดยทั่วไปคุณไม่จำเป็นต้องระบุการล็อคอย่างชัดเจน InnoDB จัดการกับมันแม้ว่าในบางสถานการณ์คุณอาจต้องให้รายละเอียดการล็อคอย่างชัดเจนเกี่ยวกับการล็อคที่ชัดเจนได้รับด้านล่าง:

รายการต่อไปนี้อธิบายถึงประเภทล็อคที่มีอยู่และเอฟเฟกต์ต่างๆ:

อ่าน

ล็อคตารางสำหรับการอ่าน ล็อค READ ล็อคตารางสำหรับแบบสอบถามที่อ่านเช่น SELECT ที่ดึงข้อมูลจากตาราง ไม่อนุญาตให้มีการดำเนินการเขียนเช่น INSERT, DELETE หรือ UPDATE ที่ปรับเปลี่ยนตารางได้แม้โดยไคลเอนต์ที่มีการล็อค เมื่อตารางถูกล็อคเพื่ออ่านไคลเอ็นต์อื่นสามารถอ่านจากตารางในเวลาเดียวกัน แต่ไม่มีไคลเอ็นต์ใดสามารถเขียนลงไปได้ ไคลเอนต์ที่ต้องการเขียนไปยังตารางที่ล็อกการอ่านต้องรอจนกว่าไคลเอ็นต์ทั้งหมดที่กำลังอ่านจากมันเสร็จสิ้นแล้วและปล่อยล็อกของพวกเขา

เขียน

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

อ่านในท้องถิ่น

ล็อคตารางสำหรับการอ่าน แต่อนุญาตการแทรกพร้อมกัน การแทรกพร้อมกันเป็นข้อยกเว้นสำหรับหลักการ "ตัวอ่านบล็อกตัวเขียน" มันใช้เฉพาะกับตาราง MyISAM หากตาราง MyISAM ไม่มีรูตรงกลางที่เกิดจากบันทึกที่ถูกลบหรือถูกอัพเดทการแทรกจะเกิดขึ้นที่ส่วนท้ายของตารางเสมอ ในกรณีนั้นไคลเอนต์ที่กำลังอ่านจากตารางสามารถล็อคด้วย READ LOCAL lock เพื่ออนุญาตให้ไคลเอนต์อื่น ๆ แทรกเข้าไปในตารางในขณะที่ไคลเอนต์ที่ถือ lock read อ่านอ่านจากมัน หากตาราง MyISAM มีรูคุณสามารถลบออกได้โดยใช้ OPTIMIZE TABLE เพื่อจัดเรียงตาราง


ขอบคุณสำหรับคำตอบ. เนื่องจากฉันมีตารางนี้และลูกค้า 100 รายกำลังตรวจสอบรายการที่รอดำเนินการฉันได้รับการชนจำนวนมาก - ลูกค้า 2-3 รายได้รับการรอแถวเดียวกัน ล็อคตารางคือช้า
Wizzard

0

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

สิ่งที่ต้องการ...

Schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime)
lastlock (int)

lastlock นั้นเป็น int เพราะมันเก็บ timestamp ของยูนิกซ์ให้ง่ายขึ้น (และอาจเร็วกว่า) เพื่อเปรียบเทียบกับ

// ขออภัยซีแมนทิกส์ฉันยังไม่ได้ตรวจสอบว่ามันทำงานได้ดี แต่ควรใกล้พอถ้าไม่

UPDATE items 
  SET lastlock = UNIX_TIMESTAMP() 
WHERE 
  lastlock = 0
  OR (UNIX_TIMESTAMP() - lastlock) > 360;

จากนั้นตรวจสอบเพื่อดูว่ามีการอัปเดตจำนวนแถวกี่แถวเนื่องจากไม่สามารถอัปเดตแถวได้สองกระบวนการพร้อมกันหากคุณอัปเดตแถวคุณจะได้ล็อค สมมติว่าคุณกำลังใช้ PHP คุณจะต้องใช้ mysql_affected_rows () ถ้าผลตอบแทนที่ได้จาก 1 เท่ากับคุณล็อคสำเร็จ

จากนั้นคุณสามารถอัปเดต lastlock เป็น 0 หลังจากที่คุณทำสิ่งที่คุณต้องทำหรือไม่ก็ขี้เกียจแล้วรอ 5 นาทีเมื่อการล็อคครั้งต่อไปจะสำเร็จ

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


-1

หรือคุณสามารถแยกส่วนเขตข้อมูลระเบียนเพื่ออนุญาตให้มีการเขียนแบบขนานและบายพาสการล็อกแถว (ลักษณะคู่ json แยกส่วน) ดังนั้นถ้าหนึ่งในฟิลด์ของบันทึกการอ่านแบบผสมเป็นจำนวนเต็ม / จริงคุณอาจมีส่วนที่ 1-8 ของฟิลด์นั้น (8 เขียนบันทึก / แถวที่มีผลบังคับใช้) จากนั้นรวมแฟรกเมนต์แบบวนไปวนหลังจากเขียนทุกครั้งลงในการค้นหาการอ่านแยกต่างหาก ทำให้ผู้ใช้งานพร้อมกันสูงสุด 8 คนพร้อมกัน

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

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

MMORPG ประสบปัญหาอย่างมากเมื่อผู้เล่นจำนวนมากเริ่มชนกันด้วยทักษะ Area of ​​Effect ผู้เล่นหลายคนทุกคนต้องเขียน / อัปเดตผู้เล่นอื่นทุกคนในเวลาเดียวกันขนานสร้างการล็อกแถวการเขียนในเร็กคอร์ดผู้เล่นรวม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.