ProblemYou need to perform transactional processing, but your application uses a nontransactional storage engine. SolutionSome transaction-like operations are amenable to workarounds such as explicit table locking. In certain cases, you might not actually even need a transaction; by rewriting your statements, you can entirely eliminate the need for a transaction. DiscussionTransactions are valuable, but sometimes they cannot or need not be used:
Grouping statements using locksIf you're using a nontransactional storage engine but you need to execute a group of statements without interference by other clients, you can do so by using LOCK TABLE and UNLOCK TABLE:[*]
Locks obtained with LOCK TABLE remain in effect until you release them and thus can apply over the course of multiple statements. This gives you the same concurrency benefits as transactions. However, there is no rollback if errors occur, so table locking is not appropriate for all applications. For example, you might try performing an operation that transfers funds from Eve to Ida as follows: LOCK TABLE money WRITE; UPDATE money SET amt = amt - 6 WHERE name = 'Eve'; UPDATE money SET amt = amt + 6 WHERE name = 'Ida'; UNLOCK TABLE; Unfortunately, if the second update fails, the effect of the first update is not rolled back. Despite this caveat, there are certain types of situations where table locking may be sufficient for your purposes:
Rewriting statements to avoid transactionsSometimes applications use transactions unnecessarily. Suppose that you have a table meeting that records meeting and convention information (including the number of tickets left for each event), and that you're writing a Ruby application containing a get_ticket( ) method that dispenses tickets. One way to implement the function is to check the ticket count, decrement it if it's positive, and return a status indicating whether a ticket was available. To prevent multiple clients from attempting to grab the last ticket at the same time, issue the statements within a transaction: def get_ticket(dbh, meeting_id) count = 0 begin dbh['AutoCommit'] = false # check the current ticket count row = dbh.select_one("SELECT tix_left FROM meeting WHERE meeting_id = ?", meeting_id) count = row[0] # if there are tickets left, decrement the count if count > 0 dbh.do("UPDATE meeting SET tix_left = tix_left-1 WHERE meeting_id = ?", meeting_id) end dbh.commit dbh['AutoCommit'] = true rescue DBI::DatabaseError => e count = 0 # if an error occurred, no tix available begin # empty exception handler in case rollback fails dbh.rollback dbh['AutoCommit'] = true rescue end end return count > 0 end The method dispenses tickets properly, but involves a certain amount of unnecessary work. It's possible to do the same thing without using a transaction at all, if auto-commit mode is enabled. Decrement the ticket count only if the count is greater than zero, and then check whether the statement affected a row: def get_ticket(dbh, meeting_id) count = dbh.do("UPDATE meeting SET tix_left = tix_left-1 WHERE meeting_id = ? AND tix_left > 0", meeting_id) return count > 0 end In MySQL, the row count returned by an UPDATE statement indicates the number of rows changed. This means that if there are no tickets left for an event, the UPDATE won't change the row and the count will be zero. This makes it easy to determine whether a ticket is available using a single statement rather than with the multiple statements required by the transactional approach. The lesson here is that although transactions are important and have their place, you may be able to avoid them and end up with a faster application as a result. The single-statement solution is an example of what the MySQL Reference Manual refers to as an "atomic operation." The manual discusses these as an efficient alternative to transactions. |