请教各位一个问题:使用asp.net 做的一个报名系统,因为课程很抢手,大概会在2分钟左右就抢完了。
系统有2张表,表1存有相关课程内容、招生人数(大约20条),表2存有人员名单信息(大概2000人)。使用mysql数据库事务来同时写入2张表,我的思路是:如果表2相应的课程已报名人数<招生人数时,则表2已报名人数+1,同时表1中报名的同学记录课程代码;否则则回滚操作。
主要SQL命令如下 (请各位原谅我这个业余使用中文表字段及变量)
SQL(0) = "update 表1 set 代码='" & 课程代码 & "' , 日期='" & 报名时间 & "' where id='" & 姓名ID & "' LIMIT 1"
SQL(1) = "update 表2 set 已报人数=已报人数+1 where 已报人数<招生人数 and 代码='" & 课程代码 & "'"
实际使用中会出现表2某一课程已报人数>表1中实际统计的该课程的数量。这种情况不一定出现在哪一课程上,系统在实际中已经运行了3次,每次都是这种结果。 也就是说有时候,回滚操作会出现SQL(1)命令已经写入,但是SQL(0)命令没有写入的情况。 可是正常情况下,事务和回滚不是都成组处理嘛,怎么还会有这种情况呢。
请教各位出现这种情况是因为我的思路的问题,还是程序设计或是数据库表的问题? 最好有类似问题的解决代码,请各位指点一二。由于本人只是个业余选手,问题描述时可能词语不太准确。
该回答引用GPTᴼᴾᴱᴺᴬᴵ
根据您的描述,可能是由于多个客户端同时访问数据库导致的竞态条件(race condition)造成的问题。具体来说,可能是在两个客户端同时尝试更新表2中的某个课程的已报人数时,由于MySQL使用的是MVCC(多版本并发控制)机制,导致其中一个客户端看到的是旧版本的已报人数,另一个客户端看到的是新版本的已报人数,最终导致已报人数的增加数量超过了招生人数限制。
·
为了避免这种情况的发生,您可以考虑使用行级锁或者事务隔离级别来解决。具体来说,您可以使用SELECT ... FOR UPDATE语句在更新表2之前锁定对应的课程记录,然后再进行更新操作。这样可以确保在同一时间只有一个客户端能够修改对应的记录,从而避免了竞态条件的发生。
·
另外,您还可以考虑将事务隔离级别设置为SERIALIZABLE,这样可以确保事务的串行化执行,从而避免了并发操作带来的问题。不过需要注意的是,这种方式会降低并发性能,因此需要根据实际情况进行权衡。
·
以下是使用SELECT ... FOR UPDATE语句的示例代码(使用C#和MySQL.Data库):
using MySql.Data.MySqlClient;
// 建立MySQL连接
MySqlConnection conn = new MySqlConnection("server=localhost;user=root;password=123456;database=test;");
conn.Open();
// 开始事务
MySqlTransaction trans = conn.BeginTransaction();
try
{
// 执行SELECT ... FOR UPDATE语句
MySqlCommand cmd = new MySqlCommand("SELECT * FROM 表2 WHERE 代码=@代码 FOR UPDATE", conn, trans);
cmd.Parameters.AddWithValue("@代码", 课程代码);
MySqlDataReader reader = cmd.ExecuteReader();
// 处理查询结果
if (reader.HasRows)
{
reader.Read();
int 已报人数 = reader.GetInt32("已报人数");
int 招生人数 = reader.GetInt32("招生人数");
if (已报人数 < 招生人数)
{
// 如果已报人数小于招生人数,则执行UPDATE语句
reader.Close();
cmd.CommandText = "UPDATE 表2 SET 已报人数=已报人数+1 WHERE 代码=@代码";
int rowsAffected = cmd.ExecuteNonQuery();
if (rowsAffected > 0)
{
// 如果UPDATE语句执行成功,则执行INSERT语句
cmd.CommandText = "INSERT INTO 表1 (代码, 姓名ID, 日期) VALUES (@代码, @姓名ID, @日期)";
cmd.Parameters.AddWithValue("@姓名ID", 姓名ID);
cmd.Parameters.AddWithValue("@日期", 报名时间);
cmd.ExecuteNonQuery();
trans.Commit();
}
else
{
// 如果UPDATE语句执行失败,则回滚事务
trans.Rollback();
}
}
else
{
// 如果已报人数等于或大于招生人数,则回滚事务
reader.Close();
trans.Rollback();
}
}
else
{
// 如果没有查询到相关记录,则回滚事务
reader.Close();
trans.Rollback();
}
}
catch (Exception ex)
{
// 出现异常时,回滚事务
trans.Rollback();
Console.WriteLine(ex.Message);
}
// 关闭连接
conn.Close();
在这个示例代码中,我们首先建立了MySQL连接,并开启了一个事务。然后,我们执行了一个SELECT ... FOR UPDATE语句,锁定了表2中代码为指定课程代码的记录,防止其他事务对该记录进行操作。接下来,我们根据查询结果进行判断,如果已报人数小于招生人数,则执行UPDATE语句和INSERT语句,如果已报人数等于或大于招生人数,则回滚事务。最后,我们关闭了MySQL连接。
使用mysql事务进行提交。看下数据库的日志等。
要想别人能帮你解决这个问题,你应该把两张表的结构发出来,不然大家就不好分析等你的问题。
从你的描述看看,表1是课程信息,但sql(0)中又有where id='" & 姓名ID & "' ,看起来好像是sql中表一和表二搞反了,和你的描述不符。
update命令,即使没有真正修改到数据(例如,where条件不满足),是不会出现异常的,也就是说,是不会触发回滚的。你应该判断update操作的返回值,如果返回值为1,就表示update成功了1条数据。