数据库的并发问题:
什么是高并发:高并发是指:在同一时间有很多人执行一个操作
例如:在某个时间段,同时有2个用户A和B要购买火车票,在购买火车票前比如要查询下火车票数据库是否有票,
与是会有一个查询的操作 select * from chepiao where chepiaoCount>0
如果查询到还有火车票的时候,就会下订单,于是就会下订单,下订单就是将数据库的车票数量减1
Update chepiao set chepiaoCount=chepiao-1
高并发出现的问题:
假如说就剩下一张火车票了,如果A,B同准备购买火车票,A先查询了一下chepiao表,发现还有一张车票,于是他就要下订单,准备更新车票表的车票数量减-1,可是就在他准备减1的时候(还没有减1),此时B用户也查询了一下车票表,发现也有一张车票,于是也准备下订单,将车票表的车票数量减1。此时A已经将车票表的车票数量减1了(更新完毕),那么车票表的实际车票数量已经为0了,因为B刚刚也查询到有一张票,于是也将车票表的车票数量减去1,这样车票表的数量就为-1了。 于是就造成了车票超卖的情况
【这种情况其实就是数据的脏读】
数据库的并发控制一:悲观锁
悲观锁的的缺点是容易引起死锁,而且性能低(一个用户读取一行数据的时候即上锁,第二个用户来读的时候需要等待第一个用户释放锁,性能能不低吗!) 一般的系统都是读数据的多,写数据的少,所以推荐用乐观锁【EF不支持悲观锁】
namespace DatabaseLock
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("请输入名字");
var myname = Console.ReadLine().Trim();
string connStr = ConfigurationManager.ConnectionStrings["ConnStr"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
using (SqlTransaction st = conn.BeginTransaction()) //开启一个事物
{
try
{
using (SqlCommand selectCmd = conn.CreateCommand())//创建查询命令
{
selectCmd.Transaction = st;
//with(xlock,ROWLOCK)表示对这一行数据加一行排他锁
//xlock表示排它锁
//ROWLOCK表示行锁
//效果:加了这个锁后,别人既不可读这行数据,也不可以写这行数据
selectCmd.CommandText = "select * from T_Girls with(xlock,ROWLOCK) where id=1";
Console.WriteLine("开始执行查询");
string bf = null;
using (SqlDataReader dr = selectCmd.ExecuteReader())
{
if (!dr.Read())
{
Console.WriteLine("没有查询到id=1的女孩");
return;
}
else
{
if (!dr.IsDBNull(dr.GetOrdinal("BF")))
{
bf = dr.GetString(dr.GetOrdinal("BF"));
}
if (!string.IsNullOrEmpty(bf))
{
if (bf == myname)
{
Console.WriteLine("早已经是我的人了");
Console.ReadKey();
return;
}
else
{
Console.WriteLine("真是一个悲伤的故事,id=1的女孩被" + bf + "抢走啦");
Console.ReadKey();
return;
}
}
}
}
using (SqlCommand updateCmd = conn.CreateCommand())//创建更新命令
{
Console.WriteLine("开始抢媳妇");
updateCmd.Transaction = st;
updateCmd.CommandText = string.Format("update T_Girls set BF='{0}' where id=1", myname);
updateCmd.ExecuteNonQuery();
Console.WriteLine("成功抢到媳妇,请按任意键结束抢女朋友这件事的事物");
Console.ReadKey();
}
st.Commit();
}
}
catch (Exception e)
{
st.Rollback();//回滚
}
}
}
}
}
}
数据库的并发控制二:乐观锁
ADO.NET版
乐观锁是一种思想,它的优点是大家性能高(大家都可以同时读,在大家都读在同一条数据的情况下,谁能更新这条数据就不一定了,最先的那个人更新这条数据后,其他人就无法更新了)
乐观锁的使用:
数据库中有一个特殊的字段类型timestamp ,列名叫什么都无所谓。这个字段的值不需要程序员去维护
每次修改这行数据的时候,对于timestamp类型的列都会自动变化(一般是增加),我们就是利用这个特性来做乐观锁
乐观锁其实根本就没有锁,它是一种思想,只是根据timestamp类型字段的特特点做了逻辑查询而已。
【火车票表chepiao字段有:id(编号), checi(车次), pnumber(车票剩余数量),rowver(timestamp类型的字段)】
假如:A和B同一时间段购买火车票,A先查询了一下数据库表
select * from chepiao where checi='G506' 【假设查出来的pnumber=1 rowver=2002】
发现还有一张票,于是准备更新车票表的数据,将G506车次的车票剩余数量减1,就在它正准备更新,还未更新的时候,B用户查询了一下车票数据表,发现也有一张票【当然与A用户查询出来的数据是一样的pnumber=1 rowver=2002】,当B用户刚刚查询完毕,A用户已经在执行更新操作了,更新语句是这样的
update chepiao set pnumber=pnumber-1 where checi='G506' and rowver='2002'
当这条语句更新完毕,这条数据的rowver字段的值会立即改变,由原来的2002 改变成2003(有可能改变成其他的值,但是一定会变就是了)
此时B用户也准备更新数据了,于是也执行了以下这段代码
update chepiao set pnumber=pnumber-1 where checi='G506' and rowver='2002'
可是此时这条数据的rowver值已经由原来的2002变成现在的2003了 因为表中没有rowver为2002的数据啊,所有这条数据执行更新的结果是“执行受影响的行数是0” 即没有更新。即B用户购买火车票失败。这就是乐观锁。
根据原理,我们用做一个抢老婆的程序体验下
乐观锁“抢老婆”的思路很简单,抢之前先查一下老婆的timestamp的值,如果查询出来的是2002,那么就执行update T_Girls set BF='张三' where id=1 and RowVersion=2002
执行之后如果发现“执行受影响的行数是0” 就说明这个老婆已经被其他人抢走了,因此,抢老婆失败
要想使用乐观锁,需要在表中加一个类型为timestamp 类型的列(名字叫啥都为所谓,我这里就给它取名RowVer吧)
namespace DatabaseLock
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("请输入名字");
var myname = Console.ReadLine().Trim();
string connStr = ConfigurationManager.ConnectionStrings["ConnStr"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
long rowver = 0;
using (SqlCommand selectCmd = conn.CreateCommand())
{
string bf=null;
Console.WriteLine("开始执行查询");
//RowVersion 是timestamp类型的,将它转换成bigint类型,好方便后面的reader.GetInt64(reader.GetOrdinal("RowVersion"));获取值
selectCmd.CommandText = "select id,name,bf,CONVERT(BIGINT,RowVersion) AS 'RowVersion' from T_Girls where id=1";
using (SqlDataReader reader = selectCmd.ExecuteReader())
{
if (!reader.Read())
{
Console.WriteLine("没有查询到id=1的女孩");
return;
}
rowver = reader.GetInt64(reader.GetOrdinal("RowVersion")); //获取到已经由timestamp类型转换成long类型的RowVersion列的值
if(!reader.IsDBNull(reader.GetOrdinal("BF"))) //如果BF的值不为空
{
bf=reader.GetString(reader.GetOrdinal("BF"));//获取BF列的值
}
if(!string.IsNullOrEmpty(bf))
{
if(bf==myname)
{
Console.WriteLine("早就是自己的人了,还抢啥?");
}
else
{
Console.WriteLine("糟糕!早就被"+bf+"抢走了");
}
Console.ReadKey();
return;
}
}
using (SqlCommand updateCmd = conn.CreateCommand())
{
updateCmd.CommandText = string.Format("update T_Girls set BF='{0}' where id=1 and RowVersion={1}", myname, rowver);
if (updateCmd.ExecuteNonQuery() > 0)
{
Console.WriteLine("成功抢到媳妇");
}
else
{
Console.WriteLine("好可惜,媳妇被别人抢走了");
}
Console.ReadKey();
}
}
}
}
}
}
EF(code first)版
首先创建数据库表
namespace BF.Entities.EntityConfig
{
public class GirlsConfig : EntityTypeConfiguration<Girls>
{
public GirlsConfig()
{
ToTable("T_Girls").HasKey(r => r.Id);
this.Property(r => r.Name).IsRequired().HasMaxLength(10);
this.Property(r => r.BF).HasMaxLength(10);
this.Property(r => r.RowVersion).IsRowVersion();//注意这里将RowVersion字段设置成IsRowVersion 那么它就会在表中生成timestamp类型的数据了
}
}
}
namespace BF.Entities.Entitys
{
public class Girls
{
public int Id { get; set; }
public string Name { get; set; }
public string BF { get; set; }
public byte[] RowVersion { get; set; } //注意:在数据表中的timestamp类型的数据这里用byte[]来接收
}
}
正题:
namespace DatabaseLock
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("请输入名字");
var myname = Console.ReadLine().Trim();
using (MyDbContext db = new MyDbContext())
{
var row = db.Girls.SingleOrDefault();
if (row != null)
{
if (!string.IsNullOrEmpty(row.BF))
{
if (row.BF == myname)
{
Console.WriteLine("早已经是你的人了,还抢啥");
}
else
{
Console.WriteLine("真可惜,美女被" + row.BF + "抢走了");
}
Console.ReadKey();
return;
}
Console.WriteLine("开始抢美女啦!");
try
{
row.BF = myname;
db.SaveChanges(); //根据我们上面查询出来的row数据,如果在更新之前,有人修改了这条数据,那么RowVer列的数据也会自动更改,那么这条数据将无法更新保存,会抛出异常,于是就会判断是抢美女失败啦
Console.WriteLine("恭喜你,成功抢到美女,下订单成功");
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine("真可惜,来晚了,美女被别人抢走了");
}
}
else
{
Console.WriteLine("没有查询到美女");
}
}
}
}
}