每次寫博客,第一句話都是這樣的:程序員很苦逼,除了會寫程序,還得會寫博客!
當然,題外話說多了,咱進入正題!
在處理大數據的時候,經常會發生並發,並發的情況發生后,會出現數據污讀,從而產生臟數據。
首先通過一段程序進行說明、<有興趣的小伙伴可以復制粘貼這段程序>。
項目背景:模擬大轉盤抽獎程序。
場下坐有近萬名群眾,他們在同一時刻同時抽獎,獎品分為一等獎:奔馳汽車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
至此本篇博客的任務就算完成了,模擬並發咱們也做到了!程序寫的比較簡單丑陋,歡迎大家積極改善發言!
@陳卧龍的博客
