不是架构的架构之三:系统基础(2)主键选择和并发

用上Live Writer写博客的感觉很好,让懒散的我也喜欢上写东西。

一个好用的软件能改变人的心情,同样一个好的架构或许也能改变一个项目的命运。


通常在一个架构体系中,主键的选择是让人头疼的事情,因为你无论选择任何一种主键,都不是最完美的,所以,只能在权衡利弊中取舍。

主键通常会有两类,一类是有业务意义的主键,例如序列号、单据号等,另一类是单纯的主键,仅仅一个唯一标识。

选择主键通常要注意这么几个问题:

1.唯一性。至少在一个数据集中是唯一的。

2.不变性。主键的值应该尽量不发生变化,我们知道,在数据库中,数据的存放和排列是按照主键的次序,在一个数据较多的表中,如果主键的值经常发生变化,那么会引起数据大量的换页和IO操作,那无疑是个巨大的性能损失。

满足上面两个条件的值,基本上就可以作为数据库的主键。由于有实际业务意义的主键,仍然不排除有频繁更新的可能,尤其是我们在实现一个架构时,所以,我们更倾向于选择单纯的主键。

单纯的主键一般有几种选择:自增int列,主键表,Guid。

几种方式可以说各有千秋,自增int列编程是最容易的,可是在数据插入到数据库之前,难以得到这条数据的主键,这样的困难对于某些场景来说是难以接受的;

主键表的方式,是目前使用较广泛的一种,就是在一个单独的表中保存表名和当前的Id,用一个存储过程来获取最新的主键,这种方式虽然在ERP系统中大量采用,但是由于在数据插入前需要进行额外的查询,性能上有所损失,另外如果获取主键的存储过程编写如果不合适,就会导致并发问题,金蝶K3系统的10.x之前的版本就有这样的并发问题;

Guid的方式是最近比较推崇的方式,由于Guid的全球唯一性,做主键是再合适不过,相对于int列虽然长度大了一些,但对于目前的存储来说,可以忽略不计;缺点也很明显,我们前面说过,数据的组织和存放是按照主键的次序来,由于Guid的产生是无序的,所以在数据量较多的时候,如果产生的Guid靠前,会导致大量数据的换页;另外一个问题也来自于性能上,通常我们的业务数据有个规律,就最近的数据查询频繁,正是由于Guid的无序性,我们查询最近的数据可能会牵扯到很多数据页,带来大量的IO操作。

于是,我们找到了一种既有Guid优点,却又规避了它的缺点的主键形式。说白了,就是用程序产生有序的Guid。

如何有序?自然想到的是时间,通过时间(精确到3毫秒)产生Guid的第一节,用系统产生的Guid补充其他的内容。

public static Guid GenerateGuid()
{
    byte[] guidArray = Guid.NewGuid().ToByteArray();

    var baseDate = new DateTime(1900, 1, 1);
    DateTime now = DateTime.Now;
    var days = new TimeSpan(now.Ticks - baseDate.Ticks);
    TimeSpan msecs = now.TimeOfDay;

    byte[] daysArray = BitConverter.GetBytes(days.Days);
    byte[] msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));

    Array.Reverse(daysArray);
    Array.Reverse(msecsArray);

    Array.Copy(daysArray,daysArray.Length - 2,guidArray,guidArray.Length - 6,2);
    Array.Copy(msecsArray,msecsArray.Length - 4,guidArray,guidArray.Length - 4,4);

    return new Guid(guidArray);
}

这段代码来自于cnblogs中某一位园友的贡献,由于时代久远,找不到该文,在此表示谢意.

 


主键选择完了,我们来看看并发问题。在教科书上,并发问题有很多分类,但是这些问题可以通过数据库的事务隔离机制来得到解决。我们今天谈的并发,是业务上的并发,事实上,这样的并发比较难以用事务之类的手段来解决。首先我们来看这类并发问题是如何产生的。

 

image

如图所示,用户B打开的单据是用户A保存之前的单据,而当用户B保存单据之后,用户A的修改就丢失了。这就是一种常见的更新丢失的并发问题。

于是,我们在数据中增加了一个时间戳的概念,时间戳在数据修改时进行验证,修改成功后立即更新。

SQLServer中有个天然的时间戳数据类型,我们可以直接用它来实现。

image

这里是日志记录下的一条数据库更新语句,在语句的where条件中,除了主键之外,还有一个Timestamp列的比较(在数据库外是byte[]类型),程序中会对Execute的结果行数进行判定,如果行数为0,那么说明更新是失败的,直接引发一个DAException异常,供表现层使用。在一条语句内完成了判定和更新两步操作,也避免了先判定后更新的时间差问题。

posted @ 2011-01-19 11:05  一味  阅读(1007)  评论(0编辑  收藏  举报