2012-06-15 12 views
7

Tôi đang tạo một trò chơi trực tuyến nhỏ, ở đâu (bạn biết gì) sẽ có nhiều người dùng truy cập cùng một cơ sở dữ liệu. Máy chủ của tôi không kích hoạt Semaphores và tôi không thể thực sự đủ khả năng cho một thứ khác (tôi là một học sinh trung học), vì vậy tôi đang tìm kiếm một giải pháp thay thế.PHP Semaphore thay thế?

Sau khi một số nghiên cứu, tôi đã đi qua một vài công việc ở quanh:


A) Tôi đã thực hiện một vài chức năng bắt chước Semaphores, đó là tốt như tôi cần tôi nghĩ:

function SemaphoreWait($id) { 
    $filename = SEMAPHORE_PATH . $id . '.txt'; 
    $handle = fopen($filename, 'w') or die("Error opening file."); 
    if (flock($handle, LOCK_EX)) { 
     //nothing... 
    } else { 
     die("Could not lock file."); 
    } 
    return $handle; 
} 

function SemaphoreSignal($handle) { 
    fclose($handle); 
} 

(Tôi biết có mã không cần thiết trong câu lệnh if). các bạn nghĩ sao? Rõ ràng nó không hoàn hảo, nhưng thực hành chung có được không? Được có bất kỳ vấn đề? Đây là lần đầu tiên tôi làm việc với đồng thời và tôi thường thích một ngôn ngữ cấp thấp, nơi nó có ý nghĩa hơn một chút.

Dù sao, tôi không thể nghĩ ra bất kỳ "sai sót logic" nào với điều này, và mối quan tâm duy nhất của tôi là tốc độ; Tôi đã đọc rằng flock là khá chậm.


B) MySQL cũng cung cấp khả năng "khóa". Điều này so sánh với flock như thế nào? Và tôi giả sử nó chặn nếu tập lệnh của một người dùng khóa bảng và một tập lệnh khác yêu cầu nó? Vấn đề tôi có thể nghĩ là điều này khóa toàn bộ bảng, nhưng tôi chỉ thực sự cần khóa trên người dùng cá nhân (vì vậy không phải ai cũng chờ đợi).


Nó có vẻ như mọi người đang nói tôi không cần Semaphores để cập nhật cơ sở dữ liệu. Tôi phát hiện ra bạn có thể sử dụng truy vấn tăng đơn giản, nhưng vẫn còn một vài ví dụ mà tôi cần phải làm việc:

Điều gì xảy ra nếu tôi cần kiểm tra giá trị trước khi thực hiện hành động? Giống như cho phép nói rằng tôi cần phải kiểm tra xem một hậu vệ là đủ mạnh trước khi cho phép một cuộc tấn công, như đủ sức khỏe. Và nếu có, hãy chiến đấu và nhận một số thiệt hại mà không để cho bất cứ ai khác muck/mess với dữ liệu.

Sự hiểu biết của tôi là có một vài truy vấn mỗi lần, qua một số mã (gửi truy vấn, tìm nạp dữ liệu, tăng, lưu trữ lại) và cơ sở dữ liệu không đủ thông minh để xử lý cái đó? Hay tôi nhầm ở đây? Xin lỗi

+0

Tâm giải thích * tại sao * bạn cần các ẩn dụ để truy cập cơ sở dữ liệu của bạn? Tôi nghĩ rằng bạn có một sự hiểu lầm lớn ở đó. – Alexander

+0

@Alexander Tôi đã thêm một ví dụ ở dưới cùng - xin lỗi nếu tôi làm! – Raekye

+0

Ah tôi chỉ đọc về tăng dần ...Có lẽ tôi nên đoán nó. Không bao giờ bắt gặp một ví dụ như vậy - vì vậy điều này làm việc tuyệt vời cho ví dụ trên và hầu hết mọi thứ, nhưng vẫn còn một vài ví dụ tôi không chắc chắn nó sẽ hoạt động như thế nào (thêm ở trên) – Raekye

Trả lời

24

Vấn đề thực sự ở đây không phải là cách thực hiện khóa, đó là cách giả mạo tính tuần tự. Cách bạn được dạy để đạt được khả năng serializability trong một thế giới trước hoặc không phải là cơ sở dữ liệu là thông qua khóa và semaphores. Ý tưởng cơ bản là:

lock() 
modify a bunch of shared memory 
unlock() 

Bằng cách đó, khi bạn có hai người dùng đồng thời, bạn có thể chắc chắn rằng không ai trong số họ tạo ra trạng thái không hợp lệ. Vì vậy, kịch bản ví dụ của bạn là một trong đó cả hai người chơi tấn công lẫn nhau và đi đến những quan điểm trái ngược nhau của những người chiến thắng. Vì vậy, bạn đang lo ngại về loại này của interleaving:

User A  User B 
    |   | 
    V   | 
    attack!  | 
    |   V 
    |   attack! 
    V   | 
    read "wins" | 
    |   V 
    |   read "wins" 
    |   | 
    V   | 
    write "wins" | 
       V 
       write "wins" 

Vấn đề là đan xen sẽ đọc và viết như thế này khiến người dùng của một ghi được ghi đè, hoặc một số vấn đề khác. Những loại vấn đề này thường được gọi là điều kiện chủng tộc vì hai chủ đề có hiệu quả đua cho cùng một tài nguyên và một trong số chúng sẽ "giành chiến thắng", người kia sẽ "mất" và hành vi không phải là điều bạn muốn.

Giải pháp có khóa, ẩn dụ hoặc phần quan trọng là tạo ra một loại nút cổ chai: chỉ có một nhiệm vụ trong phần quan trọng hoặc nút cổ chai tại một thời điểm, do đó, bộ vấn đề này không thể xảy ra. Mọi người đều cố gắng để vượt qua những nút cổ chai được chờ đợi vào bất cứ ai nhận được đầu tiên của họ để có được thông qua đỡ phải thao tác chặn:

User A  User B 
    |   | 
    V   | 
attack!  | 
    |   attack! 
    V   | 
lock   V 
    |   blocking 
    V   . 
read "wins" . 
    |   . 
    V   . 
write "wins" . 
    |   . 
    V   . 
unlock   V 
       lock 
       | 
       V 
       ... 

Một cách khác để nhìn vào điều này là sự kết hợp đọc/ghi cần phải được đối xử như một đĩa đơn đơn vị mạch lạc không thể bị gián đoạn. Nói cách khác, chúng cần phải được xử lý nguyên tử, như một đơn vị nguyên tử. Đây là chính xác những gì A trong ACID là viết tắt của, khi mọi người nói một cơ sở dữ liệu là "ACID tuân thủ." Trong cơ sở dữ liệu, chúng tôi không (hoặc ít nhất, nên giả vờ không) có ổ khóa vì chúng tôi thay vì sử dụng các giao dịch, mà phân định một đơn vị nguyên tử, như vậy:

BEGIN; 

SELECT ... 
UPDATE ... 

COMMIT; 

Tất cả mọi thứ giữa BEGINCOMMIT dự kiến được coi là một đơn vị nguyên tử, do đó, hoặc là tất cả đi hoặc không có nó. Nó chỉ ra rằng dựa vào A là chưa đủ đối với trường hợp sử dụng cụ thể của bạn, bởi vì không có cách nào giao dịch của bạn có thể thất bại lẫn nhau:

User A  User B 
    |   | 
    V   | 
BEGIN  V 
    |  BEGIN 
    V   | 
SELECT ... V 
    |  SELECT ... 
    V   | 
UPDATE  V 
    |  UPDATE 
    V   | 
COMMIT  V 
      COMMIT 

Đặc biệt nếu bạn viết những dòng này đúng cách, nơi thay vì nói UPDATE players SET wins = 37 bạn nói UPDATE players SET wins = wins + 1 , cơ sở dữ liệu không có lý do gì để nghi ngờ rằng các bản cập nhật này không thể được thực hiện song song, đặc biệt nếu chúng đang làm việc trên các hàng khác nhau. Kết quả là, bạn sẽ cần phải sử dụng thêm cơ sở dữ liệu foo: bạn cần phải lo lắng về tính nhất quán, C trong ACID.

Chúng tôi muốn thiết kế lược đồ của bạn để bản thân cơ sở dữ liệu có thể phân biệt nếu có điều gì đó không hợp lệ xảy ra, bởi vì nếu có thể, cơ sở dữ liệu sẽ ngăn chặn nó. Lưu ý: bây giờ chúng ta đang ở trong lãnh thổ thiết kế, nhất thiết phải có rất nhiều cách khác nhau để tiếp cận vấn đề. Cái tôi trình bày ở đây có thể không phải là cái tốt nhất hay thậm chí là tốt nhất, nhưng tôi hy vọng nó sẽ minh họa cho các quá trình suy nghĩ cần phải giải quyết vấn đề với cơ sở dữ liệu quan hệ.

Vì vậy, bây giờ chúng tôi quan tâm đến tính toàn vẹn , tức là cơ sở dữ liệu của bạn ở trạng thái hợp lệ trước và sau mỗi giao dịch. Nếu cơ sở dữ liệu xử lý hiệu lực dữ liệu như thế này, bạn có thể viết các giao dịch của bạn theo cách ngây thơ và bản thân cơ sở dữ liệu sẽ hủy bỏ chúng nếu chúng cố gắng làm điều gì đó không hợp lý do đồng thời. Điều này có nghĩa là chúng ta có trách nhiệm mới: chúng ta phải tìm cách để làm cho cơ sở dữ liệu nhận thức được ngữ nghĩa của bạn để nó có thể xử lý việc xác minh. Nói chung, cách đơn giản nhất để đảm bảo tính hợp lệ là với các ràng buộc khóa chính và khóa ngoài - nói cách khác, đảm bảo rằng các hàng là duy nhất hoặc chúng chắc chắn tham chiếu đến các hàng trong các bảng khác. Tôi sẽ cho bạn thấy quá trình suy nghĩ và một số lựa chọn thay thế cho hai kịch bản trong một trò chơi với hy vọng rằng bạn sẽ có thể khái quát từ đó.

Kịch bản đầu tiên sẽ bị giết. Giả sử bạn không muốn người chơi 1 có thể giết người chơi 2 nếu người chơi 2 đang ở giữa giết người chơi 1. Điều này có nghĩa là bạn muốn giết người là nguyên tử.Tôi muốn mô hình này như sau:

CREATE TABLE players (
    login VARCHAR, 
    -- password hashes, etc. 
); 

CREATE TABLE lives (
    login VARCHAR REFERENCES players, 
    life INTEGER 
); 

CREATE TABLE alive (
    login VARCHAR, 
    life INTEGER, 
    PRIMARY KEY (login, life), 
    FOREIGN KEY (login, life) REFERENCES lives 
); 

CREATE TABLE deaths (
    login VARCHAR REFERENCES players, 
    life INTEGER, 
    killed_by VARCHAR, 
    killed_by_life INTEGER, 
    PRIMARY KEY (login, life), 
    FOREIGN KEY (killed_by, killed_by_life) REFERENCES lives 
); 

Bây giờ bạn có nguyên tử có thể tạo ra cuộc sống mới:

BEGIN; 

SELECT login, MAX(life)+1 FROM lives WHERE login = 'login'; 
INSERT INTO lives (login, life) VALUES ('login', 'new life #');  
INSERT INTO alive (login, life) VALUES ('login', 'new life #'); 

COMMIT; 

Và bạn nguyên tử có thể giết chết:

BEGIN; 

SELECT name, life FROM alive 
WHERE name = 'killer_name' AND life = 'life #'; 

SELECT name, life FROM alive 
WHERE name = 'victim_name' AND life = 'life #'; 

-- if either of those SELECTs returned NULL, the victim 
-- or killer died in another transaction 

INSERT INTO deaths (name, life, killed_by, killed_by_life) 
VALUES ('victim', 'life #', 'killer', 'killer life #'); 
DELETE FROM alive WHERE name = 'victim' AND life = 'life #'; 

COMMIT; 

Bây giờ bạn có thể khá chắc chắn rằng những điều này đang xảy ra một cách nguyên tử, bởi vì ràng buộc UNIQUE ngụ ý bởi PRIMARY KEY sẽ ngăn chặn các bản ghi tử vong mới được tạo ra với cùng một người dùng và cùng một cuộc sống, bất kể o kẻ giết người có thể. Bạn cũng có thể kiểm tra thủ công các ràng buộc của bạn đang được đáp ứng, chẳng hạn như bằng cách phát hành các câu lệnh đếm giữa các bước và phát hành ROLLBACK nếu có bất kỳ điều gì bất ngờ xảy ra. Bạn thậm chí có thể gộp những thứ này vào các ràng buộc kiểm tra được kích hoạt và thêm vào các thủ tục được lưu trữ để thực sự tỉ mỉ.

Đến ví dụ thứ hai: thao tác hạn chế năng lượng. Điều đơn giản nhất để làm là thêm một hạn chế kiểm tra:

CREATE TABLE player (
    login VARCHAR PRIMARY KEY, -- etc. 
    energy INTEGER, 
    CONSTRAINT ensure_energy_is_positive CHECK(energy >= 0) 
); 

Bây giờ nếu người chơi cố gắng sử dụng năng lượng của họ hai lần, bạn sẽ nhận được một sự vi phạm hạn chế tại một trong những trình tự:

Player A #1  Player A #2 
    |    | 
    V    | 
    spell    | 
    |    V 
    V    spell 
    BEGIN    | 
    |    | 
    |    V 
    |    BEGIN 
    V    | 
    UPDATE SET energy = energy - 5; 
    |    | 
    |    | 
    |    V 
    |    UPDATE SET energy = energy - 5; 
    V    | 
    [implied CHECK: pass] 
    |    | 
    V    | 
    COMMIT   V 
        [implied CHECK: fail!] 
        | 
        V 
        ROLLBACK 

Có là những thứ khác mà bạn có thể làm để chuyển đổi thành một vấn đề toàn vẹn quan hệ chứ không phải ràng buộc kiểm tra, chẳng hạn như phát ra năng lượng trong các đơn vị đã xác định và liên kết chúng với các cá thể gọi chính tả cụ thể, nhưng có thể là quá nhiều công việc đang làm. Điều này sẽ làm việc ổn.

Bây giờ, tôi ghét phải nói điều này sau khi đặt tất cả điều đó, nhưng bạn sẽ phải kiểm tra cơ sở dữ liệu của bạn và đảm bảo rằng mọi thứ được thiết lập để tuân thủ ACID thực. Theo mặc định, MySQL được sử dụng để phân phối với MyISAM cho các bảng, có nghĩa là bạn có thể BEGIN cả ngày và mọi thứ đã được chạy riêng lẻ. Nếu bạn làm cho bảng của bạn với InnoDB như là động cơ thay vào đó, nó hoạt động nhiều hơn hoặc ít hơn như mong đợi. Tôi khuyên bạn nên thử PostgreSQL nếu bạn có thể, nó là một chút thống nhất hơn về những thứ như thế này ra khỏi hộp. Và tất nhiên cơ sở dữ liệu thương mại cũng mạnh mẽ. SQLite, mặt khác, có một khóa ghi cho toàn bộ cơ sở dữ liệu, vì vậy nếu đó là phụ trợ của bạn, toàn bộ trước đó là khá tranh luận. Tôi không khuyên bạn nên viết kịch bản đồng thời.

Tóm lại, vấn đề là cơ sở dữ liệu về cơ bản đang cố xử lý vấn đề này cho bạn theo cách rất cao. 99% thời gian, bạn chỉ đơn giản là không lo lắng về nó; tỷ lệ cược của hai yêu cầu xảy ra vào đúng thời điểm chính xác chỉ là không cao. Tất cả các hành động mà bạn quan tâm sẽ diễn ra quá nhanh, tỷ lệ cược của việc tạo ra một tình trạng cuộc đua thực sự đang biến mất nhỏ. Nhưng thật tốt khi lo lắng về điều đó. Sau khi tất cả, bạn có thể viết các ứng dụng ngân hàng một ngày nào đó và điều quan trọng là phải biết cách làm đúng. Thật không may, trong trường hợp cụ thể này, các nguyên thủy khóa mà bạn đang sử dụng để rất nguyên thủy so với cơ sở dữ liệu quan hệ đang cố gắng thực hiện. Nhưng cơ sở dữ liệu đang cố gắng tối ưu hóa cho tốc độ và tính toàn vẹn, không phải là lý luận đơn giản quen thuộc.

Nếu đây là một đường vòng thú vị cho bạn, tôi hy vọng bạn sẽ xem một trong những cuốn sách của Joe Celko hoặc sách giáo khoa cơ sở dữ liệu để biết thêm thông tin.

+0

Nhưng điều gì về kịch bản trò chơi này: Người dùng A tấn công ai đó và thắng. Vì vậy, một nơi nào đó tôi có 'chọn ...' và lấy giá trị trong cột 'thắng'. Đồng thời User B tấn công User A và thua, lấy cột 'wins' để cập nhật nó. Bây giờ họ đã cả hai nắm lấy giá trị cũ, và khi họ tăng nó và lưu trữ nó trở lại trong giá trị sẽ chỉ được tăng lên một lần. Xin lỗi, có vẻ như với tôi điều này sẽ không được xử lý vì có hai truy vấn khác nhau trên một số thời lượng mã, nhưng sửa tôi nếu tôi sai. – Raekye

+1

@Raeki, Bạn không cần phải đặt lại giá trị, chỉ cần tăng nó. 'UPDATE TABLE SET WINS = THẮNG + 1'. – Alexander

+0

Mặc dù không có lý do gì để đưa bạn vào độ tuổi mà chỉ cần cập nhật một bộ đếm giành chiến thắng như thế. Trong trường hợp đó, đó là lỗi của bạn và không phải là lỗi cơ sở dữ liệu. – Alexander