蛙蛙推荐:写一个主键生成组件(服务)

开发数据库驱动的应用时,常为表的主键选择而烦恼,常用的几种主键生成方式都各有优缺点,但在数据可移植性、读性能、写性能之间做个权衡之后觉得还是自 制+1的方式比较好。自制加1的方式的缺点是插入一条数据,需要先从数据库里获取一个新的主键ID,增加了网络往返的流量,而我改进之后是一下取一定数量 的可用主键ID缓存起来,供应用来取,这样就不必每次插入新数据都先去数据库里取新ID了。而且int型的主键,无论做聚簇索引还是非聚簇索引,肯定比字 符串、guid,COMB类型主键要快,毕竟它最窄,这就保证了读性能比较好。而写的时候大多时候从内存里读取新主键ID,也不会比identity列耗 费多少性能。关于主键生成方式的讨论请看最后的相关链接。
我的方案的缺点就是如果系统异常关闭,比如系统断电,这时候内存里的ID会丢失掉,下次启动的时候不会重复使用丢失的号,而是主键ID分配出现断层, 不过这我想了想也没什么大的影响,干嘛主键非要挨着呀,对吧。再说了,谁家的服务器也不会天天突然断电吧。你要不放心的话就在Appdomon的 unload事件里执行Stop方法,在析构函数里也执行一下stop方法,但是估计也防止不了突然断电造成的丢号现象。
另外我的程序没有做更深入的压力测试,不知道高压力高并发的情况下表现如何,我的机器实在行,赛扬1g,256内存,估计压也压不出啥有价值信息。大 家谁机器好,帮忙压一压也好哦。另外在添号和读号的时候我都加了锁,自己实现的同步,不知道是不是有更好的同步方式,高手路过的话给指点一下哦。

好,来看代码,先看接口

interface IPKGenerator
{
    
void Start();
    
void Stop();
    
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
{
    
const int maxQ = 1024;

    Queue
<int> _q = new Queue<int>(maxQ);
    
object _syncRoot = new object();
    
bool _isStop = false;
    
string _tableName;

    
static int _maxid = 0;
    
static object ctorLocker = new object();
    
static Dictionary<string, PKGenerator> _dictPKs
        
= new Dictionary<string, PKGenerator>();

    PKGenerator(
string tableName)
    
{
        _tableName 
= tableName;
    }


    
public static PKGenerator GetInstance(string tableName)
    
{
        
if (!_dictPKs.ContainsKey(tableName))
        
{
            
lock (ctorLocker)
            
{
                
if (!_dictPKs.ContainsKey(tableName))
                
{
                    PKGenerator generator 
= new PKGenerator( tableName );
                    _dictPKs.Add(tableName, generator);
                }

            }

        }

        
return _dictPKs[ tableName ]; 
    }


    
void checkIDPoll()
    
{

        
if (_q.Count != 0)
            
return;
        getMaxIDFromDB();
        
while (_q.Count <= maxQ)
        
{
            _q.Enqueue(_maxid
++);
        }

    }


    
private void getMaxIDFromDB()
    
{
        
string sql =string.Format( @"SELECT Maxid FROM G_IDPoll WHERE TableName = '{1}'
UPDATE G_IDPoll SET Maxid = Maxid + {0} WHERE TableName = '{1}'
", maxQ, _tableName);
        DbHelper db 
= new DbHelper();
        DbCommand cmd 
= db.GetSqlStringCommond(sql);
        _maxid 
= (int)db.ExecuteScalar(cmd);
    }


    
IPKGenerator 成员
}


代码很简明,为了支持多个表的主键生成,用了个字典来保存多个主键生成器的实例,可以通过表名获取新的生成器实例。每个生成器实例里有一个队列用来 缓存可用ID,取号的时候先判断队列里有没有可用号,如果没有的话就查数据库获取最大号,把缓存队列填满,然后再取号。如果取号的时候已经停止了取号服 务,则返回的号为0,所以取到号后要判断是否为0。
改进:
1、你可以用一个线程来论询缓存队列如果小于某个最低值后就把队列填满,然后读取的时候就不判断队列是否为空了,这样可以提高取号性能,但要考虑短时间内一下子把号取空的情况,所以要根据系统的压力合理的设置队列的最大值。
2、在stop里修改msxid的时候最好判断一下数据库里的最大ID是不是自己上次取出的最大ID,如果不同的话很可能有其它线程或者认为改过数据库的 值了,这时候就不要修改数据库的值了,否则就冲掉别人的修改了,这时候宁可浪费几个号,具体策略看你的业务需求。只要保证只有咱们的 PKGenerator修改那个表的话,这个判断是可以去掉的。
3、代码里没有做异常处理,为了提高程序的健壮性,大家可以自己加上。
4、取号队列的最大值可以从配置文件里取,做成可配置的
下面看看测试代码

class Program
{
    
static IPKGenerator generator;
    
static void Main(string[] args)
    
{
        
try
        
{
            generator 
= PKGenerator.GetInstance("T_Node");
            generator.Start();

            List
<Thread> t = new List<Thread>();

            
for (int i = 0; i < 10++i)
            
{
                ThreadStart ts 
= new ThreadStart(TestGetID);
                t.Add(
new Thread(ts));
            }

            
foreach (Thread thread in t)
                thread.Start();

            Thread.Sleep(
2000);
            generator.Stop();
        }

        
catch(Exception ex)
        
{
            Console.WriteLine(ex);
        }


        Console.Read();
    }


    
private static void TestGetID()
    
{
        
for (int i = 10; i > 0; i--)
            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

posted on 2007-09-23 17:07 蛙蛙池塘 阅读(1062) 评论(16)  编辑  收藏

FeedBack:
2007-09-23 17:39 | 金色海洋(jyk)
我一般使用int 自增 的。最大的问题就是合并数据的时候比较烦。
不知道你的这个能不能解决合并数据的问题。(简单看了一下,好像还是有合并的问题,除非预先考虑到合并,并且作预留)。
  回复  引用  查看    
#2楼 [未注册]
2007-09-23 17:46 | cw
不错, 我们前些天也做过类似的工作....

但是预先生成ID号似呼没有必要, 还增加复杂性, 我们是取号时才加1....
  回复  引用  查看    
#3楼 [楼主]
2007-09-23 19:00 | 蛙蛙池塘
@金色海洋(jyk)
合并数据是啥意思?你把表水平分区不就行了吗?每个分区的主键ID的初始值不一样,我做这个主要为了解决数据移植的时候,自增ID不好导的问题。@cw
@cw
我刚才也想到了,确实那个缓存队列感觉有点多余,因为这里的号码生成规则毕竟简单,只是加1,如果号码生成规则比较负责的话,预先生成一定量的可用号还是有必要的,比如说你生成的号要过滤一些靓号。


最后,我想了想,如果取走一个号,在更新数据库的时候出错了,这个号就浪费了,应该有个回收号的逻辑。
  回复  引用  查看    
2007-09-23 19:18 | 亚历山大同志
自己实现的一个序号池?有点意思,呵呵,最好能支持可变步长的序号,这样子在多服务器的情况下也可以使用。
  回复  引用  查看    
2007-09-23 19:43 | 金色海洋(jyk)
合并和你说的数据移植类似,移植是从一个表移到另一个表。

合并就是把两个表的数据合到一个表里面。

把数据从A表移到B表,但是B表已经有数据了,而且不能删除,即使 ID相同了也不能删除。

不知道你有没有遇到过这种情况。
  回复  引用  查看    
#6楼 [楼主]
2007-09-23 20:19 | 蛙蛙池塘
@亚历山大同志
恩,是的,稍微修改一下就行了,
@金色海洋(jyk)
比 如你有5台服务器,那么5台服务器的初始值ID分别为1,2,3,4,5,然后增长步长为5,那么第一台机器生成的ID都是1,6,11,16...第二 台机器的ID为2,7,12,17...,第三台机器为为3,8,13,18...依次类推,这样你合并也不会把数据冲掉哦。
  回复  引用  查看    
2007-09-23 21:10 | 金色海洋(jyk)
是呀,如果事先可以考虑到合并,那就好办了。你的方法也可以。
但是有的时候合并时不可预知的,也可能预计是五台服务器,但是后来又加了一台。你的方法就不好用了。

什么都预知好了那就好办了,问题是那些预知不到的怎么办呀?

其实另一个更好一点的方法是

1-- 999999 分给 第一台服务器
1000000--1999999 分给 第二台服务器
2000000--2999999 分给 第三台服务器
3000000--3999999 分给 第四台服务器
......



  回复  引用  查看   
原文地址:https://www.cnblogs.com/smallfa/p/904977.html