А вы уже поменяли $Increment на $Sequence?
Если бы это была запись для твиттера, то она была бы следующей:»Используйте $Sequence вместо $Increment для генерации Id». Но тут Хабр, поэтому придётся развернуть мысль — добро пожаловать под кат.
Небольшое отступление для читателей, которым и менять-то нечего, и слово $Increment они видят впервые. $Increment — встроенная функция в Caché ObjectScript — атомарная операция. Аргументом функции $Increment может быть только переменная — не выражение. $Increment неявно блокирует переменную, увеличивает её значение на 1, разблокирует переменную, возвращает новое значение. $Increment широко используется, когда нужно назначать числовой идентификатор типа счётчик новым объектам или записям, в таких случаях аргументом функции является имя глобала. Выглядит это примерно так:
for i=1:1:10000 {
set id = $Increment(^Person)
set surname = ##class(%PopulateUtils).LastName() ; случайная фамилия
set name = ##class(%PopulateUtils).FirstName() ; случайное имя
set ^Person (id) = $ListBuild(surname, name)
}
Что же такое $Sequence? Эта функция появилась в версии 2015.1, как и $Increment она выполняет атомарную операцию и возвращает увеличенное на 1 значение своего аргумента. В отличие от $Increment, аргументом $Sequence может быть только глобал (не локальная переменная). При первом обращении процесса к $Sequence от определённого глобала, $Sequence кеширует набор возвращаемых значений и при последующих обращениях возвращает значения из кеша. Значение глобала увеличивается на количество закешированных значений. Когда значения в кеше заканчиваются, $Sequence кеширует новый набор, опять увеличивая значение глобала. $Sequence автоматически определяет количество значений, которые нужно закешировать. Чем чаще процесс обращается к $Sequence, тем больше значений будет закешировано:
USER>set $Sequence(^myseq)=""
USER>for i=1:1:15 {write "increment:",$Seq(^myseq)," allocated:",^myseq,! }
increment:1 allocated:1
increment:2 allocated:2
increment:3 allocated:4
increment:4 allocated:4
increment:5 allocated:8
increment:6 allocated:8
increment:7 allocated:8
increment:8 allocated:8
increment:9 allocated:16
increment:10 allocated:16
increment:11 allocated:16
increment:12 allocated:16
increment:13 allocated:16
increment:14 allocated:16
increment:15 allocated:16
Видите, когда $Sequence (^myseq) вернула 9, следующие 8 значений (до 16) уже были закешированы для текущего процесса. Параллельный процесс, обратившийся к $Sequence (^myseq), получил бы значение 17.
$Sequence предназначена для использования в процессах, которые параллельно увеличивают одно и то же глобальное значение. Поскольку $Sequence кеширует значения порциями, то в порядке идентификаторов могут быть пропуски, если процесс использовал не все выделенные ему значения. Собственно, основное предназначение функции $Sequence — генерация уникальных значений счётчика. $Increment в этом смысле, несколько более общая функция.
Чтобы сравнить $Increment и $Sequence, запустим небольшой пример:
Class Habr.IncSeq.Test
{
ClassMethod filling ()
{
lock +^P: «S»
set job = $job
for i=1:1:200000 {
set id = $Increment(^Person)
set surname = ##class(%PopulateUtils).LastName()
set name = ##class(%PopulateUtils).FirstName()
set ^Person (id) = $ListBuild(job, surname, name)
}
lock -^P: «S»
}
ClassMethod run ()
{
kill ^Person
set z1 = $zhorolog
for i=1:1:10 {
job …filling()
}
lock ^P
set z2 = $zhorolog — z1
lock
write «done:», z2,!
}
}
Метод run запускает десять процессов, каждый из которых вставляет 200000 записей в глобал ^Person. Блокировка на глобал ^P нужна только для того, чтобы родительский процесс дождался окончания работы дочерних процессов. Поэтому он пытается получить экслюзивную блокировку на глобал ^P, но получит он её только тогда, когда все дочерние процессы доделают свою работу и снимут разделяемую блокировку; сразу после этого мы считываем отсечку времени ($zhorolog) ещё раз, снимаем полученную блокировку на ^P и смотрим, сколько секунд заняла вставка записей. На моём четырёхъядерном ноутбуке, выполнение метода run заняло 21 секунду (для зануд скажу, что это был уже пятый запуск этого же метода):
USER>do ##class(Habr.IncSeq.Test).run()
done:21.40948
Интересно узнать, на что ушла эта 21 секунда. Запустив ^%SYS.MONLBL (про который, кстати, была статья на хабре), видим cледующую картину:
; ** Source for Method 'filling' **
1 10 .000433 lock +^P:"S"
2 10 .000013 set job = $job
3 10 .000038 for i=1:1:200000 {
4 1999991 13.222959 set id = $Increment(^Person)
5 1997246 7.029486 set surname = ##class(%PopulateUtils).LastName()
6 1995420 4.766967 set name = ##class(%PopulateUtils).FirstName()
7 1999680 208.226093 set ^Person(id) = $ListBuild(job, surname, name)
8 1999790 1.69106 }
9 10 .000205 lock -^P:"S"
; ** End of source for Method 'filling' **
;
; ** Source for Method 'run' **
1 1 .01005 kill ^Person
2 1 .000003 set z1 = $zhorolog
3 1 .000004 for i=1:1:10 {
4 10 .056381 job ..filling()
5 0 0 }
6 1 26.244814 lock ^P
7 1 .000003 set z2 = $zhorolog - z1
8 1 .000006 lock
9 1 .000009 write "done:",z2,!
; ** End of source for Method 'run' **
Первый столбец в отчёте ^%SYS.MONLBL — номер строки в методе, второй — количество выполнений этой строки, третий — сколько секунд выполнялась эта строка.
В общей сложности 13.2 секунды было потрачено на получение Id. Разделив 13.2 на количество процессов, получим, что каждый из них потратил 1.32 секунды на получение нового Id, 1.1 секунды на вычисление имени и фамилии, и 20.8 секунд на запись данных в глобал. Общее время (26.24) получилось на 5 секунд больше из-за профилировщика.
Давайте заменим в нашем тесте (а именно в методе filling ()) $Increment (^Person) на $Sequence (^Person) и запустим тест ещё раз:
USER>do ##class(Habr.IncSeq.Test).run()
done:3.324123
Результат удивительный. Пусть $Sequence уменьшила время получения Id, но куда делись 20.8 секунд на запись данных? Смотрим результаты ^%SYS.MONLBL:
; ** Source for Method 'filling' **
1 10 .000523 lock +^P:"S"
2 10 .000017 set job = $job
3 10 .000048 for i=1:1:200000 {
4 1911382 1.69533 set id = $Sequence(^Person)
5 1753050 3.783609 set surname = ##class(%PopulateUtils).LastName()
6 1830006 3.407867 set name = ##class(%PopulateUtils).FirstName()
7 1827874 21.544164 set ^Person(id) = $ListBuild(job, surname, name)
8 1879819 .843424 }
9 10 .00023 lock -^P:"S"
; ** End of source for Method 'filling' **
;
; ** Source for Method 'run' **
1 1 .010926 kill ^Person
2 1 .000004 set z1 = $zhorolog
3 1 .000004 for i=1:1:10 {
4 10 .049543 job ..filling()
5 0 0 }
6 1 5.090719 lock ^P
7 1 .000003 set z2 = $zhorolog - z1
8 1 .000007 lock
9 1 .00001 write "done:",z2,!
; ** End of source for Method 'run' **
На получение Id каждый процесс теперь тратит 0.17 секунды вместо 1.32. Но почему на запись в базу тратится 2.15 секунд на процесс? Как такое может быть? Дело в том, что глобалы хранятся в блоках по (обычно) 8 килобайт каждый. Каждый процесс перед изменением глобала (таким как set ^Person (id) = …) получает внутреннюю блокировку на блок. Если несколько процессов пытаются изменить один и тот же блок — один процесс ждёт, пока другой освободит блок. Если таких процессов десять, то девять ждут одного. Если посмотреть на глобал ^Person, созданный с $increment, то можно увидеть, что почти никогда две соседние записи не созданы одним процессом:
1: ^Person(100000) = $lb("12950","Kelvin","Lydia")
2: ^Person(100001) = $lb("12943","Umansky","Agnes")
3: ^Person(100002) = $lb("12945","Frost","Natasha")
4: ^Person(100003) = $lb("12942","Loveluck","Terry")
5: ^Person(100004) = $lb("12951","Russell","Debra")
6: ^Person(100005) = $lb("12947","Wells","Chad")
7: ^Person(100006) = $lb("12946","Geoffrion","Susan")
8: ^Person(100007) = $lb("12945","Lennon","Roberta")
9: ^Person(100008) = $lb("12944","Beatty","Mark")
10: ^Person(100009) = $lb("12946","Kovalev","Nataliya")
11: ^Person(100010) = $lb("12947","Klingman","Olga")
12: ^Person(100011) = $lb("12942","Schultz","Alice")
13: ^Person(100012) = $lb("12949","Young","Filomena")
14: ^Person(100013) = $lb("12947","Klausner","James")
15: ^Person(100014) = $lb("12945","Ximines","Christine")
16: ^Person(100015) = $lb("12948","Quine","Mary")
17: ^Person(100016) = $lb("12948","Rogers","Sally")
18: ^Person(100017) = $lb("12950","Ueckert","Thelma")
19: ^Person(100018) = $lb("12944","Xander","Kim")
20: ^Person(100019) = $lb("12948","Ubertini","Juanita")
Параллельные процессы пытались пробится к одному и тому же блоку, и дольше ждали своей очереди на запись в блок, чем реально меняли данные. В случае с $Sequence, Id выдаются большими кусками, разнося разные процессы по разные блокам:
1: ^Person(100000) = $lb("12963","Yezek","Amanda")
// 351 запись с номером процесса 12963
353: ^Person(100352) = $lb("12963","Young","Lola")
354: ^Person(100353) = $lb("12967","Roentgen","Barb")
«Все это здорово», скажет читатель, но ведь при объектном и SQL-доступе Caché за нас использует $Increment для генерации новых Id. Как использовать $Sequence? Начиная с версии 2015.1 параметр хранения IDFunction определяет функцию, генерирующую Id. По умолчанию он равен «increment», но вы можете изменить его на «sequence» (В инспекторе Студии выберите Storage > Default > IDFunction)
В заключение:
Не верьте ничему, что здесь написано. Я специально не пишу характеристики компьютера и настройки экземпляра Caché, на котором я запускал этот тест — лучше запустите его сами.
Бонус
В качестве ещё одного теста я собрал небольшую ECP конфигурацию с сервером баз данных на ноутбуке и сервером приложений на виртуальной машине внутри этого ноутбука. Настроил отображение глобала ^Person в удалённую (remote, а не removed) базу. Ни о какой репрезентативности этого теста речи быть не может. $Increment c ECP нужно использовать аккуратно. Тем не менее, вот результаты:
$Increment
USER>do ##class(Habr.IncSeq.Test).run()
done:163.781288
^%SYS.MONLBL:
; ** Source for Method 'filling' **
1 10 .000503 lock +^P:"S"
2 10 .000016 set job = $job
3 10 .000044 for i=1:1:200000 {
4 1843745 1546.57015 set id = $Increment(^Person)
5 1880231 6.818051 set surname = ##class(%PopulateUtils).LastName()
6 1944594 3.520858 set name = ##class(%PopulateUtils).FirstName()
7 1816896 16.576452 set ^Person(id) = $ListBuild(job, surname, name)
8 1933736 .895912 }
9 10 .000279 lock -^P:"S"
; ** End of source for Method 'filling' **
$Sequence
USER>do ##class(Habr.IncSeq.Test).run()
done:13.826716
^%SYS.MONLBL:
; ** Source for Method 'filling' **
1 10 .000434 lock +^P:"S"
2 10 .000014 set job = $job
3 10 .000033 for i=1:1:200000 {
4 1838247 98.491738 set id = $Sequence(^Person)
5 1712000 3.979588 set surname = ##class(%PopulateUtils).LastName()
6 1809643 3.522974 set name = ##class(%PopulateUtils).FirstName()
7 1787612 16.157567 set ^Person(id) = $ListBuild(job, surname, name)
8 1862728 .825769 }
9 10 .000255 lock -^P:"S"
; ** End of source for Method 'filling' **
У функции $Sequence есть некоторые ограничения — перед использованием ознакомьтесь с документацией.
Спасибо за внимание!