开发数据库驱动的应用时,常为表的主键选择而烦恼,常用的几种主键生成方式都各有优缺点,但在数据可移植性、读性能、写性能之间做个权衡之后觉得还是自制+1的方式比较好。自制加1的方式的缺点是插入一条数据,需要先从数据库里获取一个新的主键ID,增加了网络往返的流量,而我改进之后是一下取一定数量的可用主键ID缓存起来,供应用来取,这样就不必每次插入新数据都先去数据库里取新ID了。而且int型的主键,无论做聚簇索引还是非聚簇索引,肯定比字符串、guid,COMB类型主键要快,毕竟它最窄,这就保证了读性能比较好。而写的时候大多时候从内存里读取新主键ID,也不会比identity列耗费多少性能。关于主键生成方式的讨论请看最后的相关链接。
  我的方案的缺点就是如果系统异常关闭,比如系统断电,这时候内存里的ID会丢失掉,下次启动的时候不会重复使用丢失的号,而是主键ID分配出现断层,不过这我想了想也没什么大的影响,干嘛主键非要挨着呀,对吧。再说了,谁家的服务器也不会天天突然断电吧。你要不放心的话就在Appdomon的unload事件里执行Stop方法,在析构函数里也执行一下stop方法,但是估计也防止不了突然断电造成的丢号现象。
  另外我的程序没有做更深入的压力测试,不知道高压力高并发的情况下表现如何,我的机器实在行,赛扬1g,256内存,估计压也压不出啥有价值信息。大家谁机器好,帮忙压一压也好哦。另外在添号和读号的时候我都加了锁,自己实现的同步,不知道是不是有更好的同步方式,高手路过的话给指点一下哦。
好,来看代码,先看接口
 interface IPKGenerator
interface IPKGenerator {
{ void Start();
    void Start(); void Stop();
    void Stop(); int GetNewID();
    int GetNewID(); }
}Start的时候,把数据库里的最大ID读取到内存,并填充到一定量到缓存队列里,同时给数据库的最大值加到内存队列的最大值。
Stop的时候,如果缓存队列里还有没用完的号,要把数据库里最大ID重新设置成缓存队列里的最小号,这样是防止出现号断裂的现象。
GetNewID就是获取一个新的可用的主键ID了。
Start可以在程序启动的时候调用,比如web曾许的application_start里调用,Stop在程序退出前调用。
建一个ID池表G_IDPoll,有两列,TableName列为varchar(256)类型,MaxID列为int类型,第一列保存表名,第二列保存该表里已分配出去的最大主键ID,包括缓存里的没有使用的号。
下面看实现。
 class PKGenerator : IPKGenerator
class PKGenerator : IPKGenerator {
{ const int maxQ = 1024;
    const int maxQ = 1024;
 Queue<int> _q = new Queue<int>(maxQ);
    Queue<int> _q = new Queue<int>(maxQ); object _syncRoot = new object();
    object _syncRoot = new object(); bool _isStop = false;
    bool _isStop = false; string _tableName;
    string _tableName;
 static int _maxid = 0;
    static int _maxid = 0; static object ctorLocker = new object();
    static object ctorLocker = new object(); static Dictionary<string, PKGenerator> _dictPKs
    static Dictionary<string, PKGenerator> _dictPKs = new Dictionary<string, PKGenerator>();
        = new Dictionary<string, PKGenerator>();
 PKGenerator(string tableName)
    PKGenerator(string tableName) {
    { _tableName = tableName;
        _tableName = tableName; }
    }
 public static PKGenerator GetInstance(string tableName)
    public static PKGenerator GetInstance(string tableName) {
    { if (!_dictPKs.ContainsKey(tableName))
        if (!_dictPKs.ContainsKey(tableName)) {
        { lock (ctorLocker)
            lock (ctorLocker) {
            { if (!_dictPKs.ContainsKey(tableName))
                if (!_dictPKs.ContainsKey(tableName)) {
                { PKGenerator generator = new PKGenerator( tableName );
                    PKGenerator generator = new PKGenerator( tableName ); _dictPKs.Add(tableName, generator);
                    _dictPKs.Add(tableName, generator); }
                } }
            } }
        } return _dictPKs[ tableName ];
        return _dictPKs[ tableName ];  }
    }
 void checkIDPoll()
    void checkIDPoll() {
    {
 if (_q.Count != 0)
        if (_q.Count != 0) return;
            return; getMaxIDFromDB();
        getMaxIDFromDB(); while (_q.Count <= maxQ)
        while (_q.Count <= maxQ) {
        { _q.Enqueue(_maxid++);
            _q.Enqueue(_maxid++); }
        } }
    }
 private void getMaxIDFromDB()
    private void getMaxIDFromDB() {
    { string sql =string.Format( @"SELECT Maxid FROM G_IDPoll WHERE TableName = '{1}'
        string sql =string.Format( @"SELECT Maxid FROM G_IDPoll WHERE TableName = '{1}' UPDATE G_IDPoll SET Maxid = Maxid + {0} WHERE TableName = '{1}'", maxQ, _tableName);
UPDATE G_IDPoll SET Maxid = Maxid + {0} WHERE TableName = '{1}'", maxQ, _tableName); DbHelper db = new DbHelper();
        DbHelper db = new DbHelper(); DbCommand cmd = db.GetSqlStringCommond(sql);
        DbCommand cmd = db.GetSqlStringCommond(sql); _maxid = (int)db.ExecuteScalar(cmd);
        _maxid = (int)db.ExecuteScalar(cmd); }
    }
 IPKGenerator 成员
    IPKGenerator 成员 }
}

代码很简明,为了支持多个表的主键生成,用了个字典来保存多个主键生成器的实例,可以通过表名获取新的生成器实例。每个生成器实例里有一个队列用来缓存可用ID,取号的时候先判断队列里有没有可用号,如果没有的话就查数据库获取最大号,把缓存队列填满,然后再取号。如果取号的时候已经停止了取号服务,则返回的号为0,所以取到号后要判断是否为0。
改进:
1、你可以用一个线程来论询缓存队列如果小于某个最低值后就把队列填满,然后读取的时候就不判断队列是否为空了,这样可以提高取号性能,但要考虑短时间内一下子把号取空的情况,所以要根据系统的压力合理的设置队列的最大值。
2、在stop里修改msxid的时候最好判断一下数据库里的最大ID是不是自己上次取出的最大ID,如果不同的话很可能有其它线程或者认为改过数据库的值了,这时候就不要修改数据库的值了,否则就冲掉别人的修改了,这时候宁可浪费几个号,具体策略看你的业务需求。只要保证只有咱们的PKGenerator修改那个表的话,这个判断是可以去掉的。
3、代码里没有做异常处理,为了提高程序的健壮性,大家可以自己加上。
4、取号队列的最大值可以从配置文件里取,做成可配置的
下面看看测试代码
 class Program
class Program {
{ static IPKGenerator generator;
    static IPKGenerator generator; static void Main(string[] args)
    static void Main(string[] args) {
    { try
        try {
        { generator = PKGenerator.GetInstance("T_Node");
            generator = PKGenerator.GetInstance("T_Node"); generator.Start();
            generator.Start();
 List<Thread> t = new List<Thread>();
            List<Thread> t = new List<Thread>();
 for (int i = 0; i < 10; ++i)
            for (int i = 0; i < 10; ++i) {
            { ThreadStart ts = new ThreadStart(TestGetID);
                ThreadStart ts = new ThreadStart(TestGetID); t.Add(new Thread(ts));
                t.Add(new Thread(ts)); }
            } foreach (Thread thread in t)
            foreach (Thread thread in t) thread.Start();
                thread.Start();
 Thread.Sleep(2000);
            Thread.Sleep(2000); generator.Stop();
            generator.Stop(); }
        } catch(Exception ex)
        catch(Exception ex) {
        { Console.WriteLine(ex);
            Console.WriteLine(ex); }
        }
 Console.Read();
        Console.Read(); }
    }
 private static void TestGetID()
    private static void TestGetID() {
    { for (int i = 10; i > 0; i--)
        for (int i = 10; i > 0; i--) Console.WriteLine("{0}-{1}",Thread.CurrentThread.GetHashCode(), generator.GetNewID());
            Console.WriteLine("{0}-{1}",Thread.CurrentThread.GetHashCode(), generator.GetNewID()); }
    } }
}起10个线程,并发取号,每个线程取10个号,我反复测试了几次,没有断号和出错现象,基本通过,当然了这个测试不太充分,还需要大量的测试。对了,在启动完10个线程后,主线程要睡几秒,否则有的线程还没有取完号,取号服务就停止了,有些线程就会取到好多0。
【相关链接】
蛙蛙推荐:SQLServer优化资料整理 
http://www.cnblogs.com/onlytiancai/archive/2007/09/23/903279.html
Microsoft SQL Server 2005中查询优化器使用的统计信息
http://www.microsoft.com/china/technet/prodtechnol/sql/2005/sql2005serch.mspx
数据库主键设计之思考
http://www.cnblogs.com/tintown/archive/2005/03/02/111459.html
再议《反驳 吕震宇的“小议数据库主键选取策略(原创)” 》
http://www.cnblogs.com/zhenyulu/archive/2004/07/20/25816.html
[实用源码] 线程安全/竞争读写的先进先出队列
http://www.cnblogs.com/eXcel/archive/2005/05/22/160626.html
如何:对制造者线程和使用者线程进行同步(C# 编程指南)
http://msdn2.microsoft.com/zh-cn/library/yy12yx1f(VS.80).aspx
SQL Server 索引结构及其使用(一)[转]
http://www.cnblogs.com/tintown/archive/2005/04/24/144272.html
SQL Server 索引结构及其使用(二) 
http://www.cnblogs.com/tintown/archive/2005/04/24/144273.html
SQL Server 索引结构及其使用(三) 
http://www.cnblogs.com/tintown/archive/2005/04/24/144276.html
SQL Server 索引结构及其使用(四) 
http://www.cnblogs.com/tintown/archive/2005/04/24/144277.html

