Несколько процессов, одна база данных

Prolix

Новичок
Несколько процессов, одна база данных

Здравствуйте!

Существует довольно простенькая база данных, например, с такими полями:

- уникальный идентификатор
- заголовок, по умолчанию пустой
- поле даты создания записи

Существует скрипт, который запускается каждый определенный промежуток времени (например, раз в пять минут), обходит базу по тем полям, у которых пустой title (они были занесены в нее другим сторонним скриптом) и заполняет title при помощи UPDATE на основе имеющейся функции.

Проблема может возникнуть, как можно догадаться, в том, что пока один скрипт выполняется, в этот момент может запуститься другой. И он начинает обрабатывать также те поля, которые "на себя взял" предыдущий процесс. Обработка происходит стандартнтым обращением к базе (в моем случае - pg_query... pg_fetch_array), и даже если вставить в цикл обработки SELECT условие проверки на пустой title, оно не помогает, т.к. запрос, судя по всему, и все обрабатываемые записи на его момент сохраняются в памяти.

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

Мне самому пока что представляется только один способ - в тот момент, когда запускается скрипт A, он создает некий системный файл, в который записывается дата последней обрабатываемой им записи (или же ID) - X. Когда запускается скрипт B, он считывает-запоминает данную дату и несмотря на то, что выбранные им записи частично будут "накладываться" на те, что были выбраны A, он будет обрабатывать только те записи, которые были созданы позже времени X. Соответственно, скрипт B также вносит в файл свою дату X.

Буду признателен за любые размышления по поводу, заранее спасибо!
 

karel

Guest
Не проще все это сделать на уровне СУБД - наверняка есть средства -
- транзакции и т. п
 

Prolix

Новичок
Почему транзакции? Мне нужно только в скрипте не считывать те записи, которые уже считывает другой скрипт. Возможно, это есть на уровне БД, но я не знаю, поэтому и спрашиваю. Или вы полагаете, что разумнее будет сначала сделать запись, а потом ее откатывать?

yurash - мне думается, что на запись LOCK TABLES и так делается, ибо вся проблема в том, что UPDATE там происходит не для всех записей сразу, а постепенно - данные скриптом обрабатываются несколько секунд, и только потом записывается результат для одной строчки. В этот момент туда никто кроме текущей транзакции писать не может. Или это не так? И потом все равно возникает такая ситуация - после транзакции lock снимается, и второй скрипт для той же ячейки может записать результат по второму разу, а именно этого делать не следует.

В идеале было бы, например, считывать строчки чем-то наподобие pg_fetch_array($result, LOCK_ROWS); т.е. чтобы при обращении к ним именно они блокировались, однако все остальные оставались доступными для чтения/записи. Или же, иными словами - каким-то образом получать от базы информацию, что они на данный момент уже кем-то заняты.
 

Prolix

Новичок
SELECT ... FOR UPDATE - "That is, other transactions that attempt UPDATE, DELETE, or SELECT FOR UPDATE of these rows will be blocked until the current transaction ends." Это я в мануале читал уже до того, как тему создать. Это имхо не годится (хотя именно данный вопрос и интересует), вот пример:

12:00 - Скрипт A начинает свою работу, помечает записи 1, 2, 3, 4, 5, ... N которые соответствуют условию, и начинает их обработку. На этом этапе хватает обычной блокировки записей по умолчанию, которые в базе происходят так или иначе.

12:05 - Скрипт A еще не закончил свою работу, и тут вступает в действие скрипт B. По условию, для него уже годятся только записи 4, 5, 6, 7... N, часть которых еще не успел обработать A. Но так как они заблокированы предыдущей транзакцией - то B выбирает - что? Записи, которые подходят по условию, и ждет, пока они заблокируются, а потом начинает с ними работать? либо начинает обработку сразу со "свободной" записи, если она есть?

Тут бы сразу подошел второй вариант, но вы уверены, что SELECT ... FOR UPDATE именно так и работает?
 

Ganer

Новичок
а зачем все делать в одной транзакции ?
1. скрипт делает select
2. для каждой записи из select делаем select for update для одной записи, смотрим пустой ли еще тайтл, если пустой апдейтим и коммит.

т.е. скрипт 1 и 2 могут пересечся и будут по одной на перегонки апдейтить (одну и ту же запись не смогут) пока не заполнят все записи попавшие в select первому скрипту.

-~{}~ 12.10.05 15:49:

уточнение - нада select .. for update nowait, вот только я не знаю есть ли такое в PG...
 

Prolix

Новичок
Ganer
Наши на проводе :) И, как обычно, ближе всех, но... В таком случае, вы полагаете, запись, которая была сделана select for update, каким-то образом сбрасывает то значение, которое осталось в памяти для следующего скрипта?

Скрипты 1 и 2 могут пересечься, однако при этом для скрипта 2 должны выбираться только те значения, которые еще НЕ обработал скрипт 1. И так далее - для скрипта 3 те, которые еще не обработаны 1 и 2, для скрипта 4 - те, которые еще не обработали 1, 2, 3. Я не думаю, что база это автоматом как-то регулирует, вот в чем проблема. Когда скрипт 2 считывает данные из базы, он "не видит", что они уже каким-то образом обработаны, т.е. обновленное значение для него в простом SELECT недоступно.

Испытывать страшно на production-системе, поэтому интересуюсь :)
 

Ganer

Новичок
вам бы для начала нада что-то по теории субд почитать, что такое транзакции, бликоровки и т.п. иначе врядле что-то путное получится.
попробую на пальцах: если у нас 2 скрипта начнут одновременно пытатся сделать select for update nowait, то скрипт 1 наложит блокировку на запись, а скрипту 2 это уже не удастся и он получит сообщение что запись занята и тогда зная что этой записью уже кто-то занимается можем ее пропустить и братся за следующую. скрипт 1 закончит со своей и попытается заблокировать следующую, но там скорее всего уже будет скрипт2 ... и так по кругу. хотя есть вариант что скрипт1 по какой-то причине задержался и взял запись которую скрипт2 изменив уже отпустил, вот и тут нужно проверить а есть ли тайтл и если есть то пропустить.
если скрипт2 будет двигатся навстречу первому то это сильно уменьшит конкуренцию ... и так наверно проще вычислить когда скрипты "встретились" и можно остановится.

-~{}~ 12.10.05 23:53:

нет, останавливатся нельзя, это может быть скрипт3/4, а тайтл проставил скрипт1/2 который еще шагает впереди но сейчас закончится.
 

Prolix

Новичок
и тогда зная что этой записью уже кто-то занимается можем ее пропустить и братся за следующую - так в чем и вопрос! Ключевое слово "зная" - а как узнать, что запись уже кем-то заблокирована? Ведь таким образом, можно было бы сделать select for update для всех необходимых записей сразу. Чтобы 2 скрипт начинал только со "свежих" записей.

вот и тут нужно проверить а есть ли тайтл и если есть то пропустить - именно так все и проверяется, "есть ли тайтл". Проблема в том, что начальный результат SELECT, судя по всему, сохраняется где-то в памяти, и его нужно каждый раз сбрасывать, чтобы получить более свежие данные.

Вот какой у меня код, например:

Код:
/* Подсоединяемся к базе */

$db = pg_connect ("host=$dbase_host 
dbname=$dbase_name user=$dbase_user password=$dbase_pass");

/* Запрос - помечаем все необработанные записи, старше 10 октября с пустыми заголовками */

$sql="select id, title from tables_query where
title_time > '2005-10-10 00:00:00' and title ='' ";

/* Делаем select */

if($res=pg_query($db, $sql)
and pg_num_rows($res)>0
and $row=pg_fetch_array($res,$rec,PGSQL_ASSOC) ){
$numr=pg_num_rows($res);

/* Цикл обработки данных в случае успешного запроса */

while($rec<$numr and $row=pg_fetch_array($res,$rec,PGSQL_ASSOC)){

if($row['title']!=''){

/* Вот тут и вставляется это условие.
А проблема в том, что если скрипт 1 уже
обработал часть данных из этого цикла,
то скрипт 2 все равно воспринимает их по-старому */

/* Тут происходит само обновление данных...
код там просто километровый, поэтому не приводится */

echo $row['id']."\n";

}

}

}
 

Ganer

Новичок
>Ключевое слово "зная" - а как узнать, что запись уже кем-то заблокирована?
попробывать наложить свою блокировку, т.е. select for update nowait, если запись уже кем-то заблокирована мы получим ошибку row busy

>Ведь таким образом, можно было бы сделать select for update для всех необходимых записей сразу.
нельзя, select for update никак не помечает записи - он накладывает блокировки на уровне субд, т.е. выбрать все записи которые не заблокированы ты не сможешь.
проще всего добавить поле где и помечать кто занял запись, но ты говорил что не можешь что-либо добавить.

>Проблема в том, что начальный результат SELECT, судя по всему, сохраняется где-то в памяти, и его нужно каждый раз сбрасывать, чтобы получить более свежие данные.

и это правильно, ты не сможешь получить "свежее", т.к. в следующий момент после получения select'a данные в базе могут координально изменится. у тебя 2 варианта - работать с тем что есть и надеятся что данные не изменятся (что глупо) или их заблокировать и не давать их изменить.

-~{}~ 13.10.05 15:24:

Код:
/* Подсоединяемся к базе */

$db = pg_connect ("host=$dbase_host 
dbname=$dbase_name user=$dbase_user password=$dbase_pass");

/* Запрос - помечаем все необработанные записи, старше 10 октября с пустыми заголовками */

$sql="select id, title from tables_query where
title_time > '2005-10-10 00:00:00' and title ='' ";

/* Делаем select */

if($res=pg_query($db, $sql)
and pg_num_rows($res)>0
and $row=pg_fetch_array($res,$rec,PGSQL_ASSOC) ){
$numr=pg_num_rows($res);

/* Цикл обработки данных в случае успешного запроса */

while($rec<$numr and $row=pg_fetch_array($res,$rec,PGSQL_ASSOC)){

$title = 'n/a' ;
// тут мы должны заблокировать нашу запись:
select title from tables_query where id=$row['id'] for update nowait ;

// и если тайтле еще пустой:
if($newrow['title']!='' ){

/* Вот тут и вставляется это условие.
А проблема в том, что если скрипт 1 уже
обработал часть данных из этого цикла,
то скрипт 2 все равно воспринимает их по-старому */

/* Тут происходит само обновление данных...
код там просто километровый, поэтому не приводится */

echo $row['id']."\n";



}

// а здесть мы должны поставить commit чтоб отпустить блокировку
COMMIT;

}

}
главное не забудь вырубить автокомит ! все что иде в цикле while должно быть в 1 транзакции !
 

Prolix

Новичок
попробывать наложить свою блокировку, т.е. select for update nowait, если запись уже кем-то заблокирована мы получим ошибку row busy - но таким образом, получая эту ошибку, скрипт будет переходить к следующей строке результата? Или же "ждать", пока запись "освободится"? Ожидание скрипта не устраивает полностью.

нельзя, select for update никак не помечает записи - он накладывает блокировки на уровне субд, т.е. выбрать все записи которые не заблокированы ты не сможешь. - это-то понятно. Я имел в виду, например, если у меня в коде

$sql="select id, title from tables_query where
title_time > '2005-10-10 00:00:00' and title ='' ";

заменить на

$sql="select id, title from tables_query where
title_time > '2005-10-10 00:00:00' and title ='' for update nowait";

Это может получиться? Тогда он блокирует все помечаемые записи сразу? И если к части этих записей начинает коннектиться другой скрипт, то он должен получать ошибку ROW BUSY. Или нет?
 

Ganer

Новичок
>Или же "ждать", пока запись "освободится"? Ожидание скрипта не устраивает полностью.

если укажешь nowait то ждать не будет а отвалится тут же с ошибкой row busy.

>Это может получиться? Тогда он блокирует все помечаемые записи сразу? И если к части этих записей начинает коннектиться другой скрипт, то он должен получать ошибку ROW BUSY. Или нет?

да, так и будет. второй получит row busy и отвалится ничего не сделав + на время работы скриптов у тебя будут записи заблокированы, что не вписывается в " Блокировка записей в самой базе не подходит, ибо к ней обращается много процессов, в т.ч. для записи." и обычно карается DBA.
 

Z3X

Новичок
Если речь идет о MySQL то думаю это поможет http://dev.mysql.com/doc/refman/5.1/en/lock-tables.html

- The correct way to use LOCK TABLES with transactional tables, like InnoDB, is to set AUTOCOMMIT = 0 and not to call UNLOCK TABLES until you commit the transaction explicitly. When you call LOCK TABLES, InnoDB internally takes its own table lock, and MySQL takes its own table lock. InnoDB releases its table lock at the next commit, but for MySQL to release its table lock, you have to call UNLOCK TABLES. You should not have AUTOCOMMIT = 1, because then InnoDB releases its table lock immediately after the call of LOCK TABLES, and deadlocks can very easily happen. Note that in MySQL 5.1, we do not acquire the InnoDB table lock at all if AUTOCOMMIT=1, in order to help old applications avoid unnecessary deadlocks.
 
Сверху