关于 $Sequence 函数
在本文中,我们将对比 $Increment 和 $Sequence 函数。
首先,为那些从未听说过$Increment 的读者说明一下。$Increment 是 Caché ObjectScript 中的一个函数,它执行原子操作,将参数递增 1 并返回结果值。 您只能将全局或局部变量节点作为参数传递给 $Increment,而不能传递任意表达式。$Increment 在分配序列 ID 时被广泛使用。在这种情况下,$Increment 的参数通常是一个全局节点。$Increment 保证每个使用它的进程都能获得一个唯一的 ID。
for i=1:1:10000 {
set Id = $Increment(^Person) ; new Id
set surname = ##class(%PopulateUtils).LastName() ; random last name
set name = ##class(%PopulateUtils).FirstName() ; random first name
set ^Person(Id) = $ListBuild(surname, name)
}
$Increment 的问题在于,如果多个进程并行添加行,这些进程可能会花费时间等待轮到自己,以便原子地更改存储 ID 的全局节点(如上例中的 ^Person)的值。
$Sequence是一项为解决此问题而设计的新函数。自 Caché 2015.1 起,$Sequence 已正式提供。与 $Increment 一样,$Sequence 会原子性地递增其参数的值。 与 $Increment 不同,$Sequence 会为当前进程预留一些后续计数器值,而在同一进程的下次调用中,它将直接从预留范围内返回下一个值。$Sequence 会自动计算需要预留的值数。进程调用 $Sequence 的频率越高,$Sequence 预留的值就越多:
USER>kill ^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,它将获得值 17,而不是 10。
$Sequence 专为同时递增某个全局节点的进程而设计。由于 $Sequence 会预留值,如果进程未使用所有预留的值,ID 可能会出现空缺。$Sequence 的主要用途是生成顺序 ID。与 $Sequence 相比,$Increment 是一个更通用的函数。
让我们比较一下 $Increment 和 $Sequence 的性能:
Class DC.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 通过 10 个进程执行任务,每个进程向 ^Person 全局对象插入 200’000 条记录。 等待子进程完成。方法 run 尝试获取 ^P 的独占锁。当子进程完成任务并释放 ^P 的共享锁后,主进程将获取 ^P 的独占锁并继续执行。 紧接着,我们记录系统变量 $zhorolog 的时间,并计算插入这些记录所花费的时间。我那台配备慢速硬盘的多核笔记本电脑耗时 40 秒(出于研究目的,我之前已运行过几次,因此这是第 5 次运行):
USER>do ##class(DC.IncSeq.Test).run()
done:39.198488
深入分析这40秒的过程很有意思。通过运行^%SYS.MONLBL,我们可以发现总共花费了100秒来获取ID。 100 秒 / 10 个进程 = 每个进程花费 10 秒获取新 ID,1.7 秒获取名字和姓氏,28.5 秒将数据写入全局数据变量。
下文 %SYS.MONLBL 报告的第一列是行号,第二列是该行被执行的次数,第三列是执行该行所花费的秒数。
; ** Source for Method 'filling' **
1 10 .001143 lock +^P:"S"
2 10 .000055 set job = $JOB
3 10 .000118 for i=1:1:200000 {
4 1998499 100.356554 set Id = $Increment(^Person)
5 1993866 10.409804 set surname = ##class(%PopulateUtils).LastName()
6 1990461 6.347832 set name = ##class(%PopulateUtils).FirstName()
7 1999762 285.54603 set ^Person(Id) = $ListBuild(job, surname, name)
8 1999825 3.393706 }
9 10 .000259 lock -^P:"S"
; ** End of source for Method 'filling' **
;
; ** Source for Method 'run' **
1 1 .005503 kill ^Person
2 1 .000002 set z1 = $zhorolog
3 1 .000002 for i=1:1:10 {
4 10 .201327 job ..filling()
5 0 0 }
6 1 43.472692 lock ^P
7 1 .00003 set z2 = $zhorolog - z1
8 1 .00001 lock
9 1 .000053 write "done:",z2,!
; ** End of source for Method 'run' **
由于进行了性能分析,总时间(43.47秒)比上次运行多花了4秒。
让我们在测试代码的 filling 方法中修改一处内容。我们将 $Increment(^Person) 改为 $Sequence(^Person),然后重新运行测试:
USER>do ##class(DC.IncSeq.Test).run()
done:5.135189
这个结果令人惊讶。好吧,$Sequence 确实缩短了获取 ID 的时间,但将数据存储到全局变量所耗费的 28.5 秒去哪儿了?让我们检查一下 ^%SYS.MONLBL:
; ** Source for Method 'filling' **
1 10 .001181 lock +^P:"S"
2 10 .000026 set job = $JOB
3 10 .000087 for i=1:1:200000 {
4 1802473 1.996279 set Id = $Sequence(^Person)
5 1784910 4.429576 set surname = ##class(%PopulateUtils).LastName()
6 1853508 3.829051 set name = ##class(%PopulateUtils).FirstName()
7 1838752 32.281624 set ^Person(Id) = $ListBuild(job, surname, name)
8 1951569 1.0243 }
9 10 .000219 lock -^P:"S"
; ** End of source for Method 'filling' **
;
; ** Source for Method 'run' **
1 1 .006514 kill ^Person
2 1 .000002 set z1 = $zhorolog
3 1 .000002 for i=1:1:10 {
4 10 .385055 job ..filling()
5 0 0 }
6 1 6.558119 lock ^P
7 1 .000011 set z2 = $zhorolog - z1
8 1 .000008 lock
9 1 .000025 write "done:",z2,!
; ** End of source for Method 'run' **
现在,每个进程获取 ID 所花费的时间从 10 秒缩短到了 0.2 秒。但令人不解的是,为什么每个进程存储数据只需 3.23 秒?原因在于全局节点存储在数据块中,而通常每个数据块的大小为 8192 字节。 在更改全局节点值(如 set ^Person(Id) = …)之前,进程会锁定整个数据块。如果多个进程同时尝试修改同一数据块中的数据,则只有一个进程被允许修改该数据块,其余进程必须等待该进程完成操作。
让我们来看一下使用 $Increment 生成新 ID 的全局变量。连续的记录几乎不会拥有相同的进程 ID(请记住——我们将进程 ID 存储为数据列表的第一个元素):
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 records with process number 12963
353: ^Person(100352) = $lb("12963","Young","Lola")
354: ^Person(100353) = $lb("12967","Roentgen","Barb")
如果这个示例与您项目中的情况相似,请考虑使用 $Sequence 代替 $Increment。当然,在将所有 $Increment 替换为 $Sequence 之前,请务必查阅相关文档。
当然,不要轻信这里提供的测试结果——请务必亲自进行复核。
从 Caché 2015.2 开始,您可以配置表以使用 $Sequence 代替 $Increment。为此提供了一个系统函数$system.Sequence.SetDDLUseSequence,并且在管理门户的“SQL 设置”中也可找到相同的选项。
此外,类定义中新增了一个存储参数——IDFunction,其默认值设为“increment”,这意味着将使用 $Increment 生成 ID。您可以将其更改为“sequence”(检查器 > 存储 > 默认 > IDFunction)。
额外提示
我在笔记本电脑上进行了一次快速测试:这是一个小型 ECP 配置,其中数据库服务器位于主机操作系统上,而应用服务器则位于同一台笔记本电脑上的虚拟机中。 我将 ^Person 映射到了远程数据库。这只是一个基础测试,因此我不打算据此进行普遍推论。在使用 $Increment 和 ECP 时,需要考虑一些因素。话虽如此,以下是测试结果:
使用 $Increment 时:
USER>do ##class(DC.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' **
;
; ** Source for Method 'run' **
1 1 .000045 kill ^Person
2 1 .000001 set z1 = $zhorolog
3 1 .000007 for i=1:1:10 {
4 10 .059868 job ..filling()
5 0 0 }
6 1 170.342459 lock ^P
7 1 .000005 set z2 = $zhorolog - z1
8 1 .000013 lock
9 1 .000018 write "done:",z2,!
; ** End of source for Method 'run' **
$Sequence:
USER>do ##class(DC.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' **
;
; ** Source for Method 'run' **
1 1 .000046 kill ^Person
2 1 .000002 set z1 = $zhorolog
3 1 .000004 for i=1:1:10 {
4 10 .037271 job ..filling()
5 0 0 }
6 1 14.620781 lock ^P
7 1 .000005 set z2 = $zhorolog - z1
8 1 .000013 lock
9 1 .000016 write "done:",z2,!
; ** End of source for Method 'run' **