用悲观并发方式处理数据库并发冲突以保证数据一直性的代码处理方法

悲观处理方式是 采用SQLSERVER数据库中“事务+锁”!

先上伪代码再解释:

1.先解释2个要用到的锁

SELECT * FROM table WITH (HOLDLOCK) 其他事务可以读取表,但不能更新删除

SELECT * FROM table WITH (TABLOCKX) 其他事务不能读取表,更新和删除

PS:这2个是表级锁,要锁行加上SQL过滤条件即可。数据库的默认隔离级别是readcommit,

SqlCommand cmd = new SqlCommand("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;select * from Users WITH(rowlock) where id = 2", connection, st);重设事务的隔离级别可以用行锁。

2.在一个管理页面执行Update,Delete操作的时候(起码能得到实体的主键ID数据及其修改后的数据),在代码处理的时候:

客户A在某个管理页面执行如下操作

   private void button1_Click(object sender, EventArgs e)
        {
            SqlTransaction st = null;
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
              
                try
                {
                    connection.Open();
                    using (st = connection.BeginTransaction())
                    {
               
                       
                    
                        this.Cursor = Cursors.WaitCursor;

                        SqlCommand cmd = new SqlCommand("select * from Users WITH(HOLDLOCK) where id = 2;update Users set name = 'C罗',address = 'basi' where id = 2", connection, st);
          
                        cmd.ExecuteNonQuery();

                        System.Threading.Thread.Sleep(20000);
                        st.Commit();

                        MessageBox.Show("ok。。。s1");
                    }
                }
                catch
                {
                    st.Rollback();
                }
                finally
                {
                   connection.Close();
                    this.Cursor = Cursors.Default;
                }
            }
        }

客户B在也在该管理页面执行如下操作  

              private void button1_Click(object sender, EventArgs e)
        {
 
            SqlConnection connection = new SqlConnection(connectionString);
            connection.Open();
           SqlCommand cmd = new SqlCommand("update Users set name = '大罗',address = 'basi' where id = 2", connection);
        
            cmd.ExecuteNonQuery();
                       
                        connection.Close();
                        MessageBox.Show("ok无事务的提交了");
           
        }

 

那么用户A先进入改管理页面 ,并进行修改用户ID=2的用户操作,在A用户操作20秒之内,B用户也进入该管理页面进行操作修改用户ID=2的人,那么在A用户修改操作事务提交或回滚或该事务执行失败之前,用户B的操作会等待在队列中,直到A的事务提交释放悲观锁,B的隐式事务才会提交到数据库。

这样就保证了数据的一致性。

 最后结果是用户ID=2的名字是大罗(先被改成C罗,后来改成大罗)

 

业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算

处理中,我们希望针对某个 cut-off 时间点的数据进行处理,而不希望在结算进行过程中

(可能是几秒种,也可能是几个小时),数据再发生变化。此时,我们就需要通过一些机

制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓

的 “ 锁 ” ,即给我们选定的目标数据上锁,使其无法被其他程序修改。

悲观锁( Pessimistic Locking )

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自

外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定

状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能

真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系

统不会修改数据)。

一个典型的倚赖数据库的悲观锁调用:

select * from account WITH(HOLDLOCK) where name=”Erica”

这条 sql 语句锁定了 account 表中所有符合检索条件( name=”Erica” )的记录。

本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

 

以上就是项目中的采用悲观处理方式,欢迎大家讨论自己的处理方式。

 

这里做个总结,感谢下面那位兄弟的补充。

悲观锁:交给数据库来处理的,由事务(分隐私和显式事务,平时单条SQL语句就是一个隐式事务)+锁 那控制的,其中事务相当于锁的作用域,根据事务的提交失败或回滚来释放掉显式事务中开启的锁。(事前处理)

乐观锁:是认为版本号来控制的,这种机制并发性和性能更好(事后处理)

先改覆盖:后该的覆盖先改的数据。

 

 

继续放一个例子:

程序A:

  private void button1_Click(object sender, EventArgs e)
        {
            SqlTransaction st = null;
            SqlConnection con = null;
            try
            {
                con = new SqlConnection("server=.;uid=sa;pwd=sa;database=MPRS");
                con.Open();
                st = con.BeginTransaction();
         
                SqlCommand cmd = new SqlCommand();
                cmd.CommandText = "update ActiveEmployee set LastActive = '" + DateTime.Now.ToString() + "' where EmployeeId = 19";
                cmd.Connection = con;
                cmd.Transaction = st;

                cmd.ExecuteNonQuery();
                MessageBox.Show("1"+st.IsolationLevel.ToString());

                Thread.Sleep(60000);

                cmd.CommandText = "update ActiveEmployee set LastActive = '" + DateTime.Now.ToString() + "' where EmployeeId = 39";

                cmd.ExecuteNonQuery();

                st.Commit();
            }
            catch
            {
                st.Rollback();
            }
            finally
            {
                con.Close();
            }

程序A中单击事件执行的是一个显示事务。

程序B:


        private void button1_Click(object sender, EventArgs e)
        {
            SqlTransaction st = null;
            SqlConnection con = null;
       
                con = new SqlConnection("server=.;uid=sa;pwd=sa;database=MPRS");
                con.Open();


                SqlCommand cmd = new SqlCommand();
                cmd.CommandText = "select * from ActiveEmployee where EmployeeId = 19";
                cmd.Connection = con;


                cmd.ExecuteScalar();
                MessageBox.Show("1" + st.IsolationLevel.ToString());

         
        }

 程序B中的单击事件是执行一个隐式事务。 

 

由于SQLSERVER默认的事务隔离级别是readcommit, 所以程序A执行到休眠处(没有提交事务)就锁住了第19条记录,此时如果执行程序B中事务将会等待程序A中的事务提交或回滚来释放行锁(测试发现如果程序A意外奔溃,事务没有走完,那么A事务会自动回滚并释放掉排它锁)。如果程序B中的事务查询的是该表中非19的其它记录将不会等待行锁释放。

 

 

 

事务的陷阱

  事务是在一次性完成的一组操作。虽然这些操作是单个的操作,SQL Server能够保证这组操作要么全部都完成,要么一点都不做。正是大型数据库的这一特性,使得数据的完整性得到了极大的保证。

  ---- 众所周知,SQL Server为每个独立的SQL语句都提供了隐含的事务控制,使得每个DML的数据操作得以完整提交或回滚,但是SQL Server还提供了显式事务控制语句

  BEGIN TRANSACTION 开始一个事务

  COMMIT TRANSACTION 提交一个事务

  ROLLBACK TRANSACTION 回滚一个事务

  事务可以嵌套,可以通过全局变量@@trancount检索到连接的事务处理嵌套层次。需要加以特别注意并且极容易使编程人员犯错误的是,每个显示或隐含的事物开始都使得该变量加1,每个事务的提交使该变量减1,每个事务的回滚都会使得该变量置0,而只有当该变量为0时的事务提交(最后一个提交语句时),这时才把物理数据写入磁盘。

 

 索引(Index)的使用原则

  创建索引一般有以下两个目的:维护被索引列的唯一性和提供快速访问表中数据的策略。大型数据库有两种索引即簇索引和非簇索引,一个没有簇索引的表是按堆结构存储数据,所有的数据均添加在表的尾部,而建立了簇索引的表,其数据在物理上会按照簇索引键的顺序存储,一个表只允许有一个簇索引,因此,根据B树结构,可以理解添加任何一种索引均能提高按索引列查询的速度,但会降低插入、更新、删除操作的性能,尤其是当填充因子(Fill Factor)较大时。所以对索引较多的表进行频繁的插入、更新、删除操作,建表和索引时因设置较小的填充因子,以便在各数据页中留下较多的自由空间,减少页分割及重新组织的工作。

 

 数据类型的选择

  数据类型的合理选择对于数据库的性能和操作具有很大的影响,有关这方面的书籍也有不少的阐述,这里主要介绍几点经验。

  Identify字段不要作为表的主键与其它表关联,这将会影响到该表的数据迁移。

  Text 和Image字段属指针型数据,主要用来存放二进制大型对象(BLOB)。这类数据的操作相比其它数据类型较慢,因此要避开使用。

  日期型字段的优点是有众多的日期函数支持,因此,在日期的大小比较、加减操作上非常简单。但是,在按照日期作为条件的查询操作也要用函数,相比其它数据类型速度上就慢许多,因为用函数作为查询的条件时,服务器无法用先进的性能策略来优化查询而只能进行表扫描遍历每行。

  例如:要从DATA_TAB1中(其中有一个名为DATE的日期字段)查询1998年的所有记录。

  Select * from DATA_TAB1 where datepart(yy,DATE)=1998

 

你可能感兴趣的:(数据库)