我写这篇随笔的目的旨在 澄清我在上一篇随笔 “弃用数据库自增ID,曝光一下我自己用到的解决方法“ 中的一些事实与看法,同时,我将继续在并发的问题的作题,
我将在原来的存储过程上得用锁来解决并问题并附加上我的测试代码与测试数据。
我之所以放在首页,并不是代表我这篇文章多有水平,多专业,我只是想分享一个程序员内心里深藏着的一点点设计思路与项目经验。通过分享来提高个人!
PS:我写作的初衷重在于分享自己的技术知识,对问题的设计思路以及通过分享来得到更好的建议,同样的一件东西,并不是也并不可能适合于所有人或都所有场景,就算拿来主义也需要先去其糟粕,而后取其精华!老赵原来写过一些文章的初衷被很多人误解,而且还有一部分人在不明白作者的原意初衷上进行攻击,真的是悲哀! 不过还好,老赵这厮足够扎实,顶得住压力,呵呵....
同时我针对一些评论也发下自己的看法:
有很多人说用GUID代替,这当然也是一种方法,我如果说要曝光的方法竟然是用GUID来解决数据移值、数据分割的问题,相信肯定会有大把砖头扔过来!
有人说用自增ID,自增ID也可以设置起始值,没错,看项目需要吧,我的随笔中真的没有提到一定要你用我的方法啊! 但是我总觉得插入关联数据时不方便,总是需要先拿到关键的外键值,不过有人Peter.zhu说到,可以在插入前拿到这个值,这个我还真不知道,还望告之...,
有人说( 活雷锋 Dorian Deng)每次看到为了数据移植等情况摒弃自增ID都觉得可笑... 我一直都想不通迁移的频率或者概率有多大,如果这是十年后的问题...我觉得这话说得有点绝对,未雨绸缪并不见得就是一件坏事,从全局性看问题绝对没错,没必要把自己的那点项目经验来一并否决所有项目需要。这得看是什么类型项目了,这个世界上没有绝对的事情,我就还真碰到过!
最多人说的就是并发问题,没错,确实会有并发问题,我讲得很清楚这个是它的一个缺点。不过还是有人拿它作文章,哪有十全十美的事情,不过对于一些小的应用,应该可以满足了!当然我曾经也用过!
最后,也得感谢很多人,如: llzhzhbb, 卡通一下,sinxsoft, 周强,.........
废话不多讲了!言归正传
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~separator~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1:测试前提:
测试之前,我先对原来的存储过程做一个单词的修改(当然了,我个存储过程可以优化,上一篇中有人给过不少的建议),这里我就不再罗嗦了,先看看我改后之后一个存储过程:
create procedure up_get_table_key ( @table_name varchar(50), @key_value int output ) as begin begin tran declare @key int --initialize the key with 1 set @key=1 --whether the specified table is exist if not exists(select table_name from table_key where table_name=@table_name) begin insert into table_key values(@table_name,@key) --default key vlaue:1 end -- step increase else begin select @key=key_value from table_key with (updlock) where table_name=@table_name set @key=@key+1 --update the key value by table name update table_key set key_value=@key where table_name=@table_name end --set ouput value set @key_value=@key --commit tran commit tran if @@error>0 rollback tran end
注意在查询的时候我使用了updlock,我在检索时对该行加锁,updlock的优点是允许用户读取数据(不阻塞其它事务)并在以后更新数据,同时确保自从上次读取数据后数据没有被更改。也就是说,我在本次事务调用中取到key值之后,直到更新该数据之后,锁可以保证了key值不会更改,确实不会的会话不会拿到相同的key值。
2:测试
我的测试环境:SQL server 2008,.net framework 3.5
先附上我的程序代码:
private void GetIncreateID() { while (!isStop) { int a = Database.GetIncreaseID("stud"); //int b = Database.GetIncreaseID("stud"); //int c = Database.GetIncreaseID("stud"); //int d = Database.GetIncreaseID("stud"); Database.AddStudent(a,a.ToString()); //Debug.WriteLine(string.Format("a={0},b={1},c={2},d={3}", a, b, c, d)); Thread.Sleep(200); } } protected void start_Click(object sender, EventArgs e) { Thread thread1 = new Thread(new ThreadStart(GetIncreateID)); Thread thread2 = new Thread(new ThreadStart(GetIncreateID)); Thread thread3 = new Thread(new ThreadStart(GetIncreateID)); Thread thread4 = new Thread(new ThreadStart(GetIncreateID)); thread1.Start(); thread2.Start(); thread3.Start(); thread4.Start(); for(int i = 0;i<1000;i++) { Thread thread = new Thread(new ThreadStart(GetIncreateID)); thread.Start(); } } protected void stop_Click(object sender, EventArgs e) { isStop = true; }
数据访问层代码:
public static int GetIncreaseID(string tableName) { DbCommand command = CreateCommand(); command.CommandType = System.Data.CommandType.StoredProcedure; command.CommandText = "up_get_table_key"; //GetSequence DbParameter para1 = command.CreateParameter(); para1.ParameterName = "@table_name"; para1.Value = tableName; para1.DbType = System.Data.DbType.String; para1.Size = 50; DbParameter para2 = command.CreateParameter(); para2.ParameterName = "@key_value"; para2.DbType = System.Data.DbType.Int32; para2.Direction = System.Data.ParameterDirection.Output; command.Parameters.Add(para1); command.Parameters.Add(para2); int increaseId = 0; try { command.Connection.Open(); command.ExecuteNonQuery(); increaseId = Convert.ToInt32(command.Parameters["@key_value"].Value); } catch (DbException ex) { throw ex; } finally { command.Connection.Close(); } return increaseId; } public static void AddStudent(int id, string name) { DbCommand command = CreateCommand(); command.CommandType = System.Data.CommandType.Text; command.CommandText = string.Format("insert into stud values({0},'{1}')",id,name); try { command.Connection.Open(); command.ExecuteNonQuery(); } catch (DbException ex) { throw ex; } finally { command.Connection.Close(); } }
我开启了至少1000个线程来插入数据,插入数据 id,name ,其中 id是表stud表的主键值,如果存在相同的ID值,插入数据时肯定会抛异常!
再看看我的结果:
我插入了700多万条数据,,纯粹只为测试并发性,没有发生相同的ID情况!
BTW,我根据 llzhzhbb 的建议,也试着去实现它来测试并发性!这是一种全新的做法,先看看思路:
1:定义静态int变量来取代自增ID
2:应用程序启动时初始化该静态变量为表MAX ID值
3:插入数据时,先对静态变量Interlocked.Increment自增1,然后再插入新的数据
这种做法实际就是绕过数据库,利用应用程序来解决并发性,Interlocked.Increment会以生成原子操作来进行递增并存储结果,它可以保证多线程的并发时同步性,从而使得自增ID的唯一性。
如果不是使用Interlocked.Increment会有同样的并发问题,因为很大一部分计算机上,自增操作并不是一种原子操作,因为在递增的过程中CPU会先把你要递增的变量值先读取到寄存器中,然后再对它进行自增操作,最后再把寄存器中已自增的值保存到变量中,明显,这中间发生了三个操作。因此无法保证在多线程下的并发问题。
当然,这只是一种思路,来看看它的实际应用情况吧,还是用数据说话,先看看代码:
public static class TableMappingVariable { public static int CourseMappingID = 0; static TableMappingVariable() { DbCommand command = Database.CreateCommand(); command.CommandType = System.Data.CommandType.Text; command.CommandText = "select max(cour_id) from course"; try { command.Connection.Open(); DbDataReader reader = command.ExecuteReader(); if (reader.Read()) { CourseMappingID = reader.GetInt32(0); } } catch (DbException ex) { throw ex; } finally { command.Connection.Close(); } } }
为了简单起见,我直接把它写在静态构造函数了,初始化时取最大ID值。
同样,我在测试程序中,开启了至少1000个线程来插入数据。
protected void btnStart_Click(object sender, EventArgs e) { Thread thread1 = new Thread(new ThreadStart(GetIncreateID)); Thread thread2 = new Thread(new ThreadStart(GetIncreateID)); Thread thread3 = new Thread(new ThreadStart(GetIncreateID)); Thread thread4 = new Thread(new ThreadStart(GetIncreateID)); thread1.Start(); thread2.Start(); thread3.Start(); thread4.Start(); for (int i = 0; i < 1000; i++) { Thread thread = new Thread(new ThreadStart(AddCourse)); thread.Start(); } } public void AddCourse() { while (!isStop) { Database.AddCourse(Interlocked.Increment(ref TableMappingVariable.CourseMappingID), TableMappingVariable.CourseMappingID.ToString()); Thread.Sleep(200); } } protected void btnStop_Click(object sender, EventArgs e) { isCourseStop = true; }
再看看测试数据:
我插入了400多万的数据。测试结果非常正确!这种方法相对于MAX(ID)+1来说,确实减少了数据库访问的次数,MAX(ID)+1在每次插入时都需要去表中检索一次来得到MAX(ID). 这种方法,我只需要读取一次就够了!当然不包括程序挂掉的情况........
总结,我不想讲太多,当然了,你可以继续使用GUID,自增ID,或者MAX(ID)+1.....等来作为你数据库的主键,不过这些对我来说,并不重要,我不懂金谍K3,SQLLITE,对ORACLE懂得也不是很多,更不知道什么是copmiere,更不懂得他们的设计思想,如果能公开,那就最好不过了。这个东西纯粹只为测试并发性!有可能并不适合你!
欢迎评论!
最后,附上我的测试代码:(点击下载)
PS: 顺道推荐一下我的博客音乐!真的不错!呵呵...