Is it safe to use the following code to prevent race conditions? (key and status fields and mysql_affected_rows are used to implement locking)
$mres = mysql_query("SELECT `values`, `key`, `status`
FROM `test`
WHERE `id` = 1");
$row = mysql_fetch_array($mres);
if($row['status'] != UPDATING) {
$mres = mysql_query("UPDATE `test` SET
`status` = UPDATING,
`key` = `key` + 1
WHERE `id` = 1 AND `key` = ".$row['key']);
if($mres && mysql_affected_rows()) {
//update here safely and then...
mysql_query("UPDATE `test` SET
`status` = NOT_UPDATING,
`key` = `key` + 1
WHERE `id` = 1");
}
}
My test shows that either it is not safe or I should search for a well-hidden mistake in my code.
Table is MyISAM
You should "acquire the lock" first before you retrieve values. Otherwise they may change before you get the lock.
$mres = mysql_query("UPDATE `test` SET
`status` = 'UPDATING'
WHERE `id` = 1 AND `status` = 'NOT_UPDATING'");
if ($mres && mysql_affected_rows()) {
// got the lock
// now select and update
}
id better be a unique field in the db or things may behave very weird
I couldn't see a reason to increment key
notice I quoted the strings 'UPDATING' and 'NOT_UPDATING' in sql
in your code, you should have also checked that $row['status'] had a meaningful value(what if it was false/null?) before comparing to the php constant UPDATING
hopefully you understand enough php to know that php strings should be quoted.
You can check for GET_LOCK and RELEASE_LOCK functions in MySql to simulate row locks.
http://dev.mysql.com/doc/refman/5.1/en/miscellaneous-functions.html#function_get-lock
With this approach you don't need to update rows. Also with mysql_affected_rows() if something goes wrong you may finish with always locked row (for example if you script crash before releasing row by updating it status to NOT_UPDATING). Locks granted with GET_LOCK are released automatically when connection is terminated.
Related
Currently, when a user clicks a notification on my site, I check it exists first before setting it as read. Is that wasteful? Would there be any issues if I simply did an update query?
Right now:
$check_note = $dbl->run("SELECT 1 FROM `user_notifications` WHERE `id` = ? AND `owner_id` = ?", array((int) $_GET['clear_note'], (int) $_SESSION['user_id']))->fetchOne();
if ($check_note)
{
// they have seen it and when they saw it
$dbl->run("UPDATE `user_notifications` SET `seen` = 1, `seen_date` = ? WHERE `id` = ?", array(core::$date, (int) $_GET['clear_note']));
}
Would it just be better to do this?
$dbl->run("UPDATE `user_notifications` SET `seen` = 1, `seen_date` = ? WHERE `id` = ? AND `owner_id` = ?", array(core::$date, (int) $_GET['clear_note'], (int) $_SESSION['user_id']));
Just do the update. If there's nothing that matches the WHERE condition, it won't do anything. The database has to do just as much work to test the condition during the UPDATE as it does in the SELECT, so you're not saving it any work, you're just doing an extra, redundant query.
I have this query that increments a value index in existing rows ordered by the value in the column key.
UPDATE `documents`, (
SELECT #row_number:=ifnull(#row_number, 0)+1 as `new_index`, `id`
FROM `documents`
WHERE `path` = "/path/to/doc"
ORDER BY `key`
) AS `table_position`,
(
SELECT #row_number:=0
) AS `rowNumberInit`
SET `index` = `table_position`.`new_index`
WHERE `table_position`.`id` = `documents`.`id`
and I use this PHP code to execute it:
/** #var PDO $pdo */
$ret = $pdo->query($sql);
// Now every value in column `index` is set to 1
$res = $ret->execute();
// Now every value in column `index` is counted up
This doesn't look quite like the right way to do it.
I currently use PDO directly, because Zend_Db_Adapter_Pdo_Mysql seems to wreak havoc with this query.
In addition to that I'd like to have the "/path/to/doc" string in the WHERE clause a a bind param. Replacing it by a ? and passing the value to execute() didn't work.
How would I do this correctly with Zend or PDO?
I'm looking for a solution for this:
function foo()
{
client 1 executes an update/delete
// client 2 calls foo() and breaks data integrity
client 1 executes an update/delete
}
how do I solve this with mysql? I'm using myisam tables but I'm interested to the innoDB solution as well
Note: This answer assumes that you are InnoDB which allows row level locking instead of MyISAM which requires table locks.
For cases like this, you would use transactions and READ/WRITE locks. The exact details of which you need vary from case to case, and I cannot answer that without knowing you schema and what data integrity you are worried about, so I will give you a general explanation.
A read lock can be acquired on rows which you do not intend to write to, but mustn't change for the duration of your transaction. A write lock can be acquired on rows which you intend to change at some point in the future. A transaction is a sequence of multiple actions which are applied in an all-or-nothing way to the database.
So as an example lets suppose the following:
you have 3 tables: table_A, table_B, table_C
The operation which client 1 is performing makes an update to table_A and then to
table_B.
Client 2 could be updating table any of the tables.
You require some data consistency between all 3 tables.
You would do something like this:
// This makes it so that each operation is not automatically commited (saved)
// It implicitly makes all sequences of operations into transactions
execute("set autocommit=0");
// This gets you some data from table_B and also gets a read lock to prevent that data from changing
result = execute("SELECT * FROM `table_B` WHERE `condition` = 1 LOCK IN SHARE MODE");
// This gets some data from table_C and gets a write lock to prevent the data from changing and allowing you to write to it in the future
result2 = execute("SELECT * FROM `table_C` WHERE `condition` = 1 FOR UPDATE");
// This performs your update to table_A
execute("UPDATE `table_A` SET `value` = 1234 WHERE `condition` = 1");
// This performs your update to table_C
execute("UPDATE `table_C` SET `value` = 4321 WHERE `condition` = 1");
// This saves all of the changes that you made during your transaction and releases all locks
// Note: autocommit is still turned off
execute("COMMIT");
So lets have a more concrete example involving purchasing something. I realize that this could all be done by a single update statement but I am doing it this way to illustrate how to use transactions.
My tables are:
items (id int not null primary key, user_id int not null, item_type int not null)
accounts (user_id int not null primary key, balance int not null)
prices (item_type int not null primary key, price int not null)
limits (item_type int not null primary key, max_count int not null)
Note I am going to skip input sanitation for brevity sake, DO NOT do that for real. (http://xkcd.com/327/)
function purchase(user_id, item_type) {
execute("set autocommit=0");
// I am assuming that price and max_count can be changed but they require consistency with each other hence the read locks
var price = execute("SELECT `price` FROM `prices` WHERE `item_type` = " + item_type + " LOCK IN SHARE MODE")[0].price;
var max_count = execute("SELECT `max_count` FROM `limits` WHERE `item_type` = " + item_type + " LOCK IN SHARE MODE")[0].max_count;
// I need the write lock to prevent double spending
var account = execute("SELECT * FROM `accounts` WHERE `user_id` = " + user_id + " FOR UPDATE")[0];
// I need to guarantee that the user is not over the limit
var count = execute("SELECT count(*) AS `count` FROM `items` WHERE `user_id` = " + user_id + " FOR UPDATE")[0].count;
var new_balance = account.balance - price;
if(count >= max_count || new_balance < 0) {
return false;
}
execute("INSERT INTO `items` (`user_id`, `item_type`) VALUES (" + user_id + ", " + item_type + ")");
execute("UPDATE `accounts` SET `balance` = " + new_balance + " WHERE `user_id` = " + user_id);
execute("COMMIT");
return true;
}
Also it should be noted that you now have to worry about deadlocks, but that is an entirely separate topic.
I want my the id field in my table to be a bit more " random" then consecutive numbers.
Is there a way to insert something into the id field, like a +9, which will tell the db to take the current auto_increment value and add 9 to it?
Though this is generally used to solve replication issues, you can set an increment value for auto_increment:
auto_increment_increment
Since that is both a session and a global setting, you could simply set the session variable just prior to the insert.
Besides that, you can manually do it by getting the current value with MAX() then add any number you want and insert that value. MySQL will let you know if you try to insert a duplicate value.
You have a design flaw. Leave the auto increment alone and shuffle your query result (when you fetch your data)
As far as i know, it's not possible to 'shuffle' your current IDs. If you wanted though, you could pursue non-linear IDs in the future.
The following is written in PDO, there are mysqli equivalents.
This is just an arbitrary INSERT statement
$name = "Jack";
$conn = new PDO("mysql:host=$dbhost;dbname=$dbname",$dbuser,$dbpass);
$sql = "INSERT INTO tableName (name) VALUES(:name)";
$q = $conn->prepare($sql);
$q->execute(':name' => $name);
Next, we use lastInsertId() to return the ID of the last inserted row, then we concatenate the result to rand()
$lastID = $conn->lastInsertId();
$randomizer = $lastID.rand();
Finally, we use our 'shuffled' ID and UPDATE the previously inserted record.
$sql = "UPDATE tableName SET ID = :randomizer WHERE ID=:lastID ";
$q = $conn->prepare($sql);
$q->execute(array(':lastID' => $lastID , ':randomizer' => $randomizer));
An idea.. (Not tested)
CREATE TRIGGER 'updateMyAutoIncrement'
BEFORE INSERT
ON 'DatabaseName'.'TableName'
FOR EACH ROW
BEGIN
DECLARE aTmpValueHolder INT DEFAULT 0;
SELECT AUTO_INCREMENT INTO aTmpValueHolder
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'DatabaseName'
AND TABLE_NAME = 'TableName';
SET NEW.idColumnName =aTmpValueHolder + 9;
END;
Edit : If the above trigger doesn't work try to update AUTO_INCREMENT value directly into the system's schema. But as noted by Eric, your design seems to be flawed. I don't see the point of having an auto-increment here.
Edit 2 : For a more 'random' and less linear number.
SET NEW.idColumnName =aTmpValueHolder + RAND(10);
Edit 3 : As pointed out by Jack Williams, Rand() produces a float value between 0 and 1.
So instead, to produce an integer, we need to use a floor function to transform the 'random' float into an integer.
SET NEW.idColumnName =aTmpValueHolder + FLOOR(a + RAND() * (b - a));
where a and b are the range of the random number.
I'm using $this->db->update(); to create an update query that adds the value stored in a variable, $amount, to the value in a column, count. My function call currently looks like this:
$data = array('count' => 'count + '.$amount);
$this->db->where('id', $item_id);
$this->db->update('items', $data);
However, this generates the following broken SQL:
UPDATE `items` SET `count` = 'count + 2' WHERE `id` = '2'
Is there a way to generate the SET clause without the quotes around count + 2?
Thanks, Maxime Morin, for putting me on the right track. According to the CodeIgniter Documentation, you can create a "set" clause without quotes by setting the optional $escape parameter to FALSE. Thus, the solution to my problem was:
$this->db->set("count", "count + $amount", FALSE);
$this->db->where("id", $item_id);
$this->db->update("items", $data);
Here's the work-around I used until I found my accepted solution
$query = $this->db->query("UPDATE `items` SET `count` = `count` + $amount WHERE `id` = $item_id");