Обновление данных в 2-х таблицах (MyISAM) БД. Возможна ли потеря данных при сбоях.

Гриша К.

Новичок
Обновление данных в 2-х таблицах (MyISAM) БД. Возможна ли потеря данных при сбоях.

Добрый вечер : )

Запутался я сильно с транзакциями при изучении, с блокировками таблиц для MyISAM, и вообще с сохранением целостности данных для нетранзакционных таблиц, и возможно вопросы мои несколько не по теме сообщения получаются.

# К примеру есть каталог сайтов, в котором информация о сайтах сортируется в зависимости от кол-ва денежных средств оплаченных за размещение сайта (СТАВКА).
Информация о сайте и ставке содержится в таблице БД orders(orderid, customerid, site, rate)

# Пользователь имеет возможность не оплачивать сразу денежные средства для повышения ставки сайта, а хранить информацию о переведенных денежных средствах на счет фирмы, занимающейся данными услугами, непосредственно на сайте, т.е. клиент имеет СЧЕТ на сайте, с которого может списывать деньги в пользу повышения ставки сайта.
Информация о пользователе и его счете хранится в таблице БД customers(customerid, username, account)

# Пользователь может размещать несколько сайтов и изменять ставки для нескольких сайтов сразу.


Теоретический механизм данных операций следующий:
осуществляется запрос к БД в цикле, на одновременное обновление поля account (СЧЕТ) и rate (СТАВКА) в двух объединенных таблицах (по полю customerid) orders и customers

Практический механизм выглядит так:
PHP:
/* $site_array - массив содержащий результат запроса select к БД на извлечение данных о клиенте и его сайтах (счет, ставка, url сайта) из 2-х объединенных таблиц orders [b]orders[/b] и [b]customers[/b]
*/
foreach ($site_array as $key => $value) {
// Проверка необходимых условий

       /* Запрос к БД: в данном случае со счета клиента списывается 0.50$ и зачисляется в пользу ставки сайта 0.50$ */
       $result = mysql_query("update customers, orders
                                             set customers.account = '9.50', orders.rate = '0.50'
                                             where customers.customerid = orders.customerid and orders.orderid = '$orderid'");
       if (!$result) {
                $error = 'Невозможно сохранить данные';
       }
}

ВОПРОСЫ:
(1) Возможно ли при таком варианте утеря данных, т.е. со счета клиента спишутся данные (customers.account), но в пользу ставки сайта не запишутся (orders.rate).
(2) Данную ставку получается может изменять только один пользователь, по сути другого такого потока быть не может. Возможно только, что модератор может к примеру изменять эту же ставку, например в качестве бонуса, тогда возможно есть смысл изменить некоторые строки так:
PHP:
set customers.account = customers.account - 0.50, orders.rate = orders.rate + 0.50

В документации MySQL (MYSQL - справочник по языку, Компания MySQK AB, Издательство Вильямс 2005), я читал про транзакции и атомарные операции, также читал что к примеру когда используется два запроса к базе данных, один к примеру списывает средства со счета сберкнижки, а другой зачисляет на счет чековой книжки, то используются специальные методы для нетранзакционных таблиц, чтобы данные не были утеряны, но там не написано про то, что если две таблицы объеденены.
 

Romantik

TeaM PHPClub
Гриша К.
IMHO не стоит для этого использовать MyISAM. Есть же InnoDB с поддержкой транзакций. В чем проблема?
 

Гриша К.

Новичок
Romantik, спасибо за ответ.
Я знаю что есть InnoDB, но я хотел бы использовать MyISAM, еще и потому что я постоянно обращяюсь к указанным таблица с запросами содержащими COUNT() и поэтому хотел бы вопрос как правильно организовать работу в нетранзакционных таблицах для описанного примера.

Вот, что я нашел в руководстве MySQL по работе с нетранзакционными таблицами, для поддержки строгих требований целостности данных даже на нетранзакционных таблицах MyISAM:
Можно также использовать функции для обновления записей за одну операцию. Можно получить весьма эффективные приложения, применяя следующую технику:

- Модифицировать столбцы в соответсвии с их текущими значениями.
- Обновлять только те столбцы, которые изменились.

UPDATE tablename SET pay_back=pay_back+125
UPDATE customers
SET
customers_date='current_date',
address='new address',
phone='new phone',
money_owed_to_us=money_owed_to_us-125
WHERE
cutomer_id=id AND address='old address' AND phone='ol phone';

Такой подход весьма эффективен и работает, даже если другой клиент изменяет значения столбцов pay_back и money_owed_to_us.
Но мне здесь не понятен тот, моент, что если таблицы tablename обновится, а таблица customers, не обновится по причине сбоя сервера MySQL, что тогда делать?

И если используется один оператор UPDATE для двух объединенных таблиц, то возможно ли, что тогда одна таблица обновится, а другая нет? - Может в моем случае применение такого варианта будет эффектифно?
 

Romantik

TeaM PHPClub
велосипет
у тебя не задается начало транзакции и ее конец.
В данном случае даже блокировки не спасут.
 

FractalizeR

Новичок
ВОПРОСЫ:
(1) Возможно ли при таком варианте утеря данных, т.е. со счета клиента спишутся данные (customers.account), но в пользу ставки сайта не запишутся (orders.rate).
Нет, невозможно. В случае с нетранзакционными таблицами каждый SQL оператор атомарен. Он либо выполняется, либо нет. Но частично он выполниться не может.
("In transactional terms, MyISAM tables effectively always operate in AUTOCOMMIT=1 mode." )

Каждый SQL оператор работает только с завершенные предыдущим оператором данными. Иными словами, когда вы делаете SELECT по таблице, вы можете быть уверены, что не возникнет ситуации, когда SELECT выполняется в тот момент, когда изменена только половина данных предыдущим UPDATE.
("MySQL Server also guarantees that there are no dirty reads. ")

(2) Данную ставку получается может изменять только один пользователь, по сути другого такого потока быть не может. Возможно только, что модератор может к примеру изменять эту же ставку, например в качестве бонуса, тогда возможно есть смысл изменить некоторые строки так:

set customers.account = customers.account - 0.50, orders.rate = orders.rate + 0.50
Да, мне кажется, этот вариант много лучше.

Но у вас может возникнуть другая проблема из-за нетранзакционных таблиц. Может случиться так, что выполниться только половина цикла foreach, потом MySQL даст сбой или завершится скрипт по какой-то причине и вторая половина foreach цикла останется невыполненной. Или это не столь важно?
 

Romantik

TeaM PHPClub
Да как не важно?
Если сбой, то нужно откатить все запросы которые были в блоке транзакции.
Конечно можно свою реализацию написать но это уже параноя
 

si

Administrator
FractalizeR
с myisam ты никак не можешь гарантировать целостность данных и точка.
 

Гриша К.

Новичок
Спасибо всем за ответы.
У меня практически все страницы на сайте обращаются к описанным таблицам, и на всех страницах запрос к базе данных содержит COUNT(), т.е я извлекаю кол-во записей, например для постраничного вывода данных.

Вот что написано по поводу использования оператора COUNT, для таблиц InnoDB в документации:
Для транзакционных механизмов хранения сохранение явного кол-ва строк проблематично, поскольку одновременно может быть активно множество транзакци, каждая из которых изменяет кол-во строк.

Если я изменю тип хранения описанных таблиц (customers и orders) на InnoDB:
1) Как мне узнавать действительное кол-во нужных записей в этих таблицах, для постраничного вывода к примеру.
2) Смогу ли я спокойно объядинять таблицы типа InnoDB, с MyISAM.
3) На одной из страниц сайта я использую следующую конструкцию, для вывода данных имеющих максимальную ставку:
Строка, содержащая максимальное значение некоторого столбца - http://dev.mysql.com/doc/refman/4.0/ru/example-maximum-column-group-row.html
PHP:
CREATE TEMPORARY TABLE tmp (
        article INT(4) UNSIGNED ZEROFILL DEFAULT '0000' NOT NULL,
        price   DOUBLE(16,2)             DEFAULT '0.00' NOT NULL);

LOCK TABLES shop read;

INSERT INTO tmp SELECT article, MAX(price) FROM shop GROUP BY article;

SELECT shop.article, dealer, shop.price FROM shop, tmp
WHERE shop.article=tmp.article AND shop.price=tmp.price;

UNLOCK TABLES;

DROP TABLE tmp;


Я сейчас попробовал просто изменить тип хранения данных двух таблиц customers и orders на InnoDB:
ALTER TABLE customers ENGINE=InnoDB;
ALTER TABLE orders ENGINE=InnoDB;


Посмотрел, что сраницы сайта работают также, никаких визуальных задержек и сбоев я не увидел. С таблицами MyISAM, получается, что вроде обединение проходит нормально.

Если действительно имзенение типа данных ниоказывает особого влияния на работу сайта, по одним и тем же запросам, то возможно тогда действительно буду использовать для некоторых таблиц тип хранения данных InnoDB.
 

Romantik

TeaM PHPClub
1) не встречался с подобной проблемой
2) но тогда откат на MyISAM не подействует
3) Все равно нужно перепроверять максимальную ставку так как она может измениться во времени.
 

Гриша К.

Новичок
Romantik, спасибо за ответ.

2) Так как данные у меня изменяются только в 2-х таблицах orders и customers, а остальные таблицы содержат только информацию, необходимую для вывода данных, и так как метод транзакций мне нужно использовать только когда проходит операция между таблицами customers и orders, то в таком случае я думаю, что возможо я могу использовать в остальных таблицах тип MyISAM. К примеру у меня есть таблица содержащая, справочник городов cities(cityid, city), есть ли смысл для нее использовать тип данных InnoDB.
Я правда так сильно запутался, возможно тогда стоит все таблицы изменить на тип данных InnoDB (тогда как я понимаю они буду занимать на много больше места и дольше загружаться).

3) Как это делать я пока не могу понять, потому что я вообще незнаю как использовать транзакции и читая документацию, всеравно немогу разобраться в этом.
Могли бы вы мне помоч, мой пример, привести в пример использующий транзакции.
В докуметации я нашел такой пример:
PHP:
mysql>
SET AUTOCOMMIT=0;
START TRNSACTION;
SELECT @A:=SUM(salary) FROM TABLE1 WHERE type=1;
UPDATE table2 SET summary=@A WHERE type=1;
COMMIT;
Щас хотел написть какой-то пример, пото понимаю, что какя-то ерунда получаестя, я совсе запутлася.
 

Romantik

TeaM PHPClub
и дольше загружаться
тут зависит от грамотности написания запросов и оптимизация.
тест транзакции оч просто
PHP:
$db->query('SET AUTOCOMMIT=0;');
$query1="UPDATE...";
$db->query($query1);
$query2="INSERT...";
$db->query($query2);
$db->query('COMMIT;');

// ну и попробуй между двумя запросами сделать exit; и посмотри в базе
// в обработчике запросов если ошибка то
$db->query('ROLLBACK;');
посмотри реализацию PEAR :: DB или ADODB
 

Гриша К.

Новичок
Romantik, Спасибо за ответ.
Я так понял, что в примере приведенном вами вы используете библиотеку mysqli из PHP5 для подключения к БД.

Я купил новую книгу: (разработка Web-приложений) с помощью PHP и MYSQL, Люк Веллинг и Лора Томсон, третье издание + CD.
У меня есть второе издание этой книги, но в новом издании очень много нового, и как раз там описан пример транзакции для таблиц InnoDB, там же я прочитал и про mysqli.

Как я понял, приведенный в первом сообщении пример, я могу обработать с помощью транзаций для таблицы InnoDB таким образом:

PHP:
$conn = db_connect(); // function db_connect() { ... @mysql_pconnect('localhost'); ... } 

// Отключаю режим автоматического подтверждения
mysql_query(set autocommit = 0);

/* В книге написано: Если режим автоматического подтверждения отключен, эта команда ненужна, 
потому что транзакция будет автоматически  запущена после ввода оператора SQL. 
*/
// mysql_query(start transaction);


foreach ($site_array as $key => $value) { 
// Проверка необходимых условий 

       /* Запрос к БД: в данном случае со счета клиента списывается 0.50$ и зачисляется в пользу ставки сайта 0.50$ */ 
       $result = mysql_query("update customers, orders 
                      set customers.account = customers.account  - 0.50, orders.rate = orders.rate + 0.50
                      where customers.customerid = orders.customerid and orders.orderid = '$orderid'"); 
       if (!$result) { 
                $error = 'Невозможно сохранить данные'; 
       } 
}

mysql_query(commit); // Подтверждаю операции, либо rollback - отмена.
mysql_query(set autocommit = 1); // Включаю обратно режим автоматического подтверждения
И на случай того, если вдруг до этих операций были операции на изменение этих же данных, использую в операторе set такой вариант:
PHP:
set customers.account = customers.account  - 0.50, orders.rate = orders.rate + 0.50
А не такой: set customers.account = 9.50, orders.rate = 0.50


И так понимаю, что все операции входящие в транзакцию, будут либо все выполнены, либо не одной.

У меня щас ощущение того, что я все понял, если что-то не так в приведенном примере, то напишите поажлуйста.

-~{}~ 20.04.06 23:58:
 

dr-sm

Новичок
Автор оригинала: Гриша К.
PHP:
$conn = db_connect(); // function db_connect() { ... @mysql_pconnect('localhost'); ... } 

// Отключаю режим автоматического подтверждения
mysql_query(set autocommit = 0);

/* В книге написано: Если режим автоматического подтверждения отключен, эта команда ненужна, 
потому что транзакция будет автоматически  запущена после ввода оператора SQL. 
*/
// mysql_query(start transaction);
 ..........
mysql_query(commit); // Подтверждаю операции, либо rollback - отмена.
mysql_query(set autocommit = 1); // Включаю обратно режим автоматического подтверждения
не нужно автокоммит использовать для начала транзакции, выкинь свою книжку :).

PHP:
$error = ! @mysql_query('start tran');

..... lalala ...............

if (!$error) {
  @mysql_query('commit tran');
} else {
  @mysql_query('rollback tran');
}
И на случай того, если вдруг до этих операций были операции на изменение этих же данных, использую в операторе set такой вариант:
PHP:
set customers.account = customers.account  - 0.50, orders.rate = orders.rate + 0.50
А не такой:
PHP:
set customers.account = 9.50, orders.rate = 0.50
это на самом деле, две абсолютно разных операции (с точки зрения mySQL).

И так понимаю, что все операции входящие в транзакцию, будут либо все выполнены, либо не одной.
да, транзакция атомарна.

мне кажется, что ты сильно недооцениваешь скорость работы InnoDB, myISAM вроде хранит ОБЩЕЕ кол-во записей в таблице, те если есть в запросе where то всеравно будет index scan. но если уж сильно сомневаешься то можно вынести изменяющиеся данные в отдельные InnoDB таблицы. а потом смело делать inner join в select'ах. да и если у тебя версия mySQL, которая поддерживает view, то вместо плясок с временными таблицами сделай один раз:
create view max_price_view as
SELECT article, MAX(price) FROM shop GROUP BY article;

-~{}~ 21.04.06 11:09:

да забыл добавить, что в InnoDB есть еще такая вкуснятина, как foreign keys.
 

Гриша К.

Новичок
dr-sm, спасибо большое за разъяснения.
Помойму теперь, с вашей помощью, я разабрался.

:: В руководстве MySQL я нашел как раз, то что можно использовать только START TRANSATION; - для отдельной последовательности операторов.

# Я думал, что если между операторами START TRANSATION; и COMMIT; - один из sql-запросов не будет выполнен, то остальные запросы будут отменены автоматически.
# Разбив свой sql запрос на 2, и во 2-м намеренно сделав неверное имя столбца, я увидел, что 1-ый всерано выполняется.
# ПРАВИЛЬНО ДЕЛАТЬ НАДО ТАК: если один из sql запросов невыполнен, то делаем откат командой
'rollback', а если все нормально, то подтверждаем все запросы входящие в транзакцию 'commit'.

Приведу такой пример:

PHP:
$error = ! @mysql_query('start tran'); // В случае ошибки возвращает 1

foreach ($site_array as $key => $value) { 

       // Снимаю со счета 0.50$
       $result_customers = mysql_query("update customers  
                                        set account = account  - 0.50
                                        where customerid = $value['customerid']");  

       // Кладу в оплату повышения ставки 0.50$
       $result_orders = mysql_query("update orders  
                                  set rate = rate + 0.50 
                                  where customerid = $value['customerid'] and orderid = $value['orderid']"); 

       // Если один из запросов невыполнен, определяю $error
       if (!$result_customers || !$result_orders) {  
                $error = 'Невозможно сохранить данные';  
       }  
}

// Если ошибок не было, т.е. все запросы выполнены успешно, то завершаю транзакцию, иначе делаю откат транзакции
if (!$error) { 
        mysql_query('COMMIT');
}
else { 
       mysql_query('ROLLBACK');
}
Вот например если в данном примере, во втаром запрое ($result_customers) сделать неверное имя стоблца 'orders111' (вместо 'order'), то в итоге первый запрос будет отменен.

Теперь я действительно разобрался, за что еще раз спасибо dr-sm.

Буду также разбираться с внешними ключами (foreign keys).


Так, как я неправильно понимал работу транзакций, я и неправильно смотрел в книгу.
Данный пример у них представлен в виде функции, и случае неуспешного выполнения запроса, они завершают функцию "return false;",
в книге все верно написано, а из-за своего непонимания я еще и книгу плохо приподнес. И то что, они используют autocomit = 0, у них это обосновано.


-~{}~ 21.04.06 20:52:

Я попробовал открыть два соединения к БД:
В 1-м соединении к БД я запустил 'START TRANSACTION', сделал изменение в таблице 'Table1'.
Затем во 2-м соединение к БД попытался сделать изменение в той же таблице 'Table1', но у видел, что запрос невыполнился, а встал в режим ожидания.
После того как в 1-м соединение к БД я ввел команду 'COMMIT', во втором соединении запрос выполнился.

Значит буду я изменять столбец так 'set account = account - 0.50' или так 'set account = 9.50', разницы нет никакой.
И вообще получается очень хорошо, что данные на момент незавершенной транзакции не могут быть изменены в другом потоке данных.
 

FractalizeR

Новичок
Автор оригинала: si
FractalizeR
с myisam ты никак не можешь гарантировать целостность данных и точка.
Не нужно точек. Данные могут быть повреждены только в результате сбоя сервера.
 
Сверху