每次寫博客,第一句話都是這樣的:程序員很苦逼,除了會寫程序,還得會寫博客!
當然,題外話說多了,咱進入正題!
在處理大數據的時候,經常會發生並發,並發的情況發生后,會出現數據污讀,從而產生臟數據。
首先通過一段程序進行說明、<有興趣的小伙伴可以復制粘貼這段程序>。
項目背景:模擬大轉盤抽獎程序。
場下坐有近萬名群眾,他們在同一時刻同時抽獎,獎品分為一等獎:奔馳汽車10輛,二等獎:別克汽車20輛,三等獎:現代汽車30輛。(獎品信息存入數據庫)
獎品信息如下(數據庫部分):
create table JP_test
(
Id int identity(1,1),
JpLeave int,--獎品等級
JpName nvarchar(50),--獎品名稱
jpCount int,--獎品數量
)
insert into JP_test values(1,'奔馳汽車',10)
insert into JP_test values(2,'別克汽車',20)
insert into JP_test values(3,'現代汽車',30)
update JP_test set jpCount=10 where JpLeave=1
update JP_test set jpCount=20 where JpLeave=2
update JP_test set jpCount=30 where JpLeave=3
--以上SQL語句大家看懂表示沒壓力。在此不作講解。
程序思路:通過開辟線程進行模擬操作,一個線程代表一個群眾,由於群眾近萬人,我們暫且開辟十個線程並通過FOR循環進行模擬!
條件判斷:當數據庫中一等獎數據小於1時,不再進行一等獎的抽獎工作,也就是不再進行針對一等獎數量的減少工作。同理二等獎,三等獎。
最后輸出抽獎后的一等獎,二等獎,三等獎數量。
按照我們的設計思路,很顯然最后的答案應該為:一等獎,二等獎,三等獎數量都為0,真實情況是否如此?請看下面代碼。
模擬代碼如下:
class Program { static T_SQL imp = new T_SQL();//數據庫操作類,這個很簡單,大家可利用自己現有的類進行數據庫操作。在此不作解釋。 /// <summary> /// 場景:模擬大轉盤抽獎 X個人同時抽獎 直至獎品被抽完。 /// </summary> static void Main(string[] args) { Task td1 = Task.Factory.StartNew(choujiang); Task td2 = Task.Factory.StartNew(choujiang); Task td3 = Task.Factory.StartNew(choujiang); Task td4 = Task.Factory.StartNew(choujiang); Task td5 = Task.Factory.StartNew(choujiang); Task td6 = Task.Factory.StartNew(choujiang); Task td7 = Task.Factory.StartNew(choujiang); Task td8 = Task.Factory.StartNew(choujiang); Task td9 = Task.Factory.StartNew(choujiang); Task td10 = Task.Factory.StartNew(choujiang);//開辟十條線程 Task.WaitAll(td1, td2, td3, td4, td5, td6, td7, td8, td9, td10);//十條線程同時執行抽獎方法,並進行抽獎。 Thread.Sleep(1000); string sql = "select JpLeave,jpCount from JP_test";//讀取抽獎后獎品數量 System.Data.DataTable dt = new System.Data.DataTable(); dt = imp.GetSqlDataSet(System.Data.CommandType.Text, sql).Tables[0]; for (int i = 0; i < dt.Rows.Count; i++) { Console.WriteLine(dt.Rows[i]["JpLeave"] + "等獎剩余數量為:" + dt.Rows[i]["jpCount"]);//輸出各個獎項的數量 } Console.ReadKey(); } /// <summary> /// 模擬抽獎程序 /// </summary> /// <returns></returns> public static void choujiang() { for (int i = 0; i < 1000; i++) { Random ran = new Random(); int NX = ran.Next(1, 101);//隨機數,不作解釋。不懂得小伙伴可自行百度。 StringBuilder sb = new StringBuilder(); sb.Append("update JP_test set jpCount=jpCount-1 where 1=1 "); if (NX < 11)//抽中一等獎 { sb.Append(" and JpLeave=1"); string sql = "select jpCount from JP_test where JpLeave=1"; int count = Convert.ToInt32(imp.GetSqlOne(System.Data.CommandType.Text, sql));//獲取當前一等獎數量 if (count > 0)//如果數據庫中一等獎數量大於0,則數量減少1 { imp.GetSqlCount(System.Data.CommandType.Text, sb.ToString());//執行數據庫 } } else if (10 < NX && NX < 21)//抽中二等獎 { sb.Append(" and JpLeave=2"); string sql = "select jpCount from JP_test where JpLeave=2"; int count = Convert.ToInt32(imp.GetSqlOne(System.Data.CommandType.Text, sql));//獲取當前二等獎數量 if (count > 0)//如果數據庫中2等獎數量大於0,則數量減少1 { imp.GetSqlCount(System.Data.CommandType.Text, sb.ToString());//執行數據庫 } } else//抽中三等獎 { sb.Append(" and JpLeave=3"); string sql = "select jpCount from JP_test where JpLeave=3"; int count = Convert.ToInt32(imp.GetSqlOne(System.Data.CommandType.Text, sql));//獲取當前三等獎數量 if (count > 0)//如果數據庫中3等獎數量大於0,則數量減少1 { imp.GetSqlCount(System.Data.CommandType.Text, sb.ToString());//執行數據庫 } } } } }
T_SQL代碼如下:
public class T_SQL { public static string connstr = "Data Source=SZ11120020;Initial Catalog=WZ_shop;Integrated Security=True;"; SqlConnection conn = null; SqlCommand cmd = null; SqlDataAdapter adapter = null; SqlDataReader reader = null; DataSet ds = null; //本段代碼的缺點是:沒執行一個SQL語句,都會創建一次連接對象。因此效率低下。 /// <summary> /// 用提供的函數,執行SQL命令,返回一個從指定連接的數據庫記錄集 /// </summary> /// <remarks> /// 例如: /// SqlDataReader r = ExecuteReader(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24)); /// </remarks> /// <param name="connectionString">SqlConnection有效的SQL連接字符串</param> /// <param name="commandType">CommandType:CommandType.Text、CommandType.StoredProcedure</param> /// <param name="commandText">SQL語句或存儲過程</param> /// <param name="commandParameters">SqlParameter[]參數數組</param> /// <returns>SqlDataReader:執行結果的記錄集</returns> public SqlDataReader GetSqlReader(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) { SqlCommand cmd = new SqlCommand(); SqlConnection conn = new SqlConnection(connstr); // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常, // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。 try { PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms); SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection); cmd.Parameters.Clear(); return rdr; } catch { conn.Close(); throw; } } /// <summary> /// 為執行命令做好准備:打開數據庫連接,命令語句,設置命令類型(SQL語句或存儲過程),函數語取。 /// </summary> /// <param name="cmd">SqlCommand 組件</param> /// <param name="conn">SqlConnection 組件</param> /// <param name="trans">SqlTransaction 組件,可以為null</param> /// <param name="cmdType">語句類型:CommandType.Text、CommandType.StoredProcedure</param> /// <param name="cmdText">SQL語句,可以為存儲過程</param> /// <param name="cmdParms">SQL參數數組</param> private void PrepareCommand(SqlCommand cmd, SqlConnection conn, SqlTransaction trans, CommandType cmdType, string cmdText, SqlParameter[] cmdParms) { if (conn.State != ConnectionState.Open) conn.Open(); cmd.Connection = conn; cmd.CommandText = cmdText; if (trans != null) cmd.Transaction = trans; cmd.CommandType = cmdType; if (cmdParms != null) { foreach (SqlParameter parm in cmdParms) cmd.Parameters.Add(parm); } } private void PrepareCommand(OleDbCommand cmd, OleDbConnection conn, OleDbTransaction trans, CommandType cmdType, string cmdText, OleDbParameter[] cmdParms) { if (conn.State != ConnectionState.Open) conn.Open(); cmd.Connection = conn; cmd.CommandText = cmdText; if (trans != null) cmd.Transaction = trans; cmd.CommandType = cmdType; if (cmdParms != null) { foreach (OleDbParameter parm in cmdParms) cmd.Parameters.Add(parm); } } public OleDbDataReader GetOleReader(CommandType cmdType, string cmdText, params OleDbParameter[] cmdParms) { OleDbCommand cmd = new OleDbCommand(); OleDbConnection conn = new OleDbConnection(connstr); // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常, // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。 try { PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms); OleDbDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection); cmd.Parameters.Clear(); return rdr; } catch { conn.Close(); throw; } } /// <summary> /// 用提供的函數,執行SQL命令,返回一個從指定連接的數據庫記錄集 /// </summary> /// <remarks> /// 例如: /// int count = cmd.ExecuteNonQuery(); /// </remarks> /// <param name="connectionString">SqlConnection有效的SQL連接字符串</param> /// <param name="commandType">CommandType:CommandType.Text、CommandType.StoredProcedure</param> /// <param name="commandText">SQL語句或存儲過程</param> /// <param name="commandParameters">SqlParameter[]參數數組</param> /// <returns>SqlDataReader:執行結果的記錄集</returns> public int GetSqlCount(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) { SqlCommand cmd = new SqlCommand(); SqlConnection conn = new SqlConnection(connstr); // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常, // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。 try { PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms); int count = cmd.ExecuteNonQuery(); cmd.Parameters.Clear(); return count; } catch { conn.Close(); throw; } finally { conn.Close(); } } public int GetOleCount(CommandType cmdType, string cmdText, params OleDbParameter[] cmdParms) { OleDbCommand cmd = new OleDbCommand(); OleDbConnection conn = new OleDbConnection(connstr); // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常, // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。 try { PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms); int count = cmd.ExecuteNonQuery(); cmd.Parameters.Clear(); return count; } catch { conn.Close(); throw; } finally { conn.Close(); } } public DataSet GetOleDataSet(CommandType cmdType, string cmdText, params OleDbParameter[] cmdParms) { OleDbCommand cmd = new OleDbCommand(); OleDbConnection conn = new OleDbConnection(connstr); // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常, // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。 try { PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms); OleDbDataAdapter da = new OleDbDataAdapter(); da.SelectCommand = cmd; DataSet ds = new DataSet(); da.Fill(ds, "tablename"); return ds; } catch { conn.Close(); throw; } finally { conn.Close(); } } public DataSet GetSqlDataSet(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) { SqlCommand cmd = new SqlCommand(); SqlConnection conn = new SqlConnection(connstr); // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常, // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。 try { PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms); SqlDataAdapter da = new SqlDataAdapter(); da.SelectCommand = cmd; DataSet ds = new DataSet(); da.Fill(ds, "tablename"); return ds; } catch { conn.Close(); throw; } finally { conn.Close(); } } public object GetOleOne(CommandType cmdType, string cmdText, params OleDbParameter[] cmdParms) { OleDbCommand cmd = new OleDbCommand(); OleDbConnection conn = new OleDbConnection(connstr); // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常, // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。 try { PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms); object one = cmd.ExecuteScalar(); return one; } catch { conn.Close(); throw; } finally { conn.Close(); } } public object GetSqlOne(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms) { SqlCommand cmd = new SqlCommand(); SqlConnection conn = new SqlConnection(connstr); // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常, // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。 try { PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms); object one = cmd.ExecuteScalar(); return one; } catch { conn.Close(); throw; } finally { conn.Close(); } } }
下面我們看看程序執行的結果(因為近萬名群眾同時抽獎,同時與數據庫進行交互,所以程序運行會占用15秒左右的時間,請大家耐心等待):
呵呵:最后一等獎,二等獎的數量都變成了負數(當然三等獎也可能為負數)!如果這個活動是公司年會抽獎,那么你注定不好過年了!哈哈哈!
究其原因,就是咱們在操作數據庫的時候,沒有進行並發處理。有興趣的小虎伴們也可以嘗試: Thread td1 = new Thread(choujiang);進行操作!在此就不作演示了!
要想不被老板罵,建議:看我的上篇博客,C#並發處理 鎖OR線程安全。網址:http://www.cnblogs.com/chenwolong/p/LoveFuTing.html及SQL樂觀鎖及悲觀鎖 http://www.cnblogs.com/chenwolong/p/Lock.html
至此本篇博客的任務就算完成了,模擬並發咱們也做到了!程序寫的比較簡單丑陋,歡迎大家積極改善發言!
@陳卧龍的博客