在数据库中如何高效的实现订座功能
前言
本文主要给大家介绍了关于在数据库中如何高效的实现订座功能的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧
第一部分:SKIPLOCKED/NOWAIT订座功能实现
订座在现实生活中是一种很常见的场景,比较常见的有火车票席位选择,电影院席位选择等等。那么如何实现订座功能呢?应用程序可能有很多种不同的实现方式,当然,肯定离不开数据库。这里将介绍一种纯数据库的实现方式。
设想我们有一张座位表如下:
CREATETABLEseats( seat_noINTPRIMARYKEY, bookedENUM('YES','NO')DEFAULT'NO')ENGINE=InnoDB;
表中有100个席位,从0到99。例如我们要预定席位2,3,我们可以先开启事务,锁定席位:
STARTTRANSACTION;SELECT*FROMseatsWHEREseat_noIN(2,3)ANDbooked='NO'FORUPDATE;
SELECT…FORUPDATE语句返回结果有如下三种情况:
1.返回成功,并且结果集包含2和3,那么说明锁定成功。我们可以之行下一步操作,等待支付完成,并更新席位状态并提交事务,订座完成。UPDATEseatsSETbooked='YES'WHEREseat_noIN(2,3)COMMIT;
2.返回成功,但结果集为空,或者只包含2或者3,那么说明锁定失败。
3.很长时间不返回直到返回超时。比如席位2或者3已经被另一事务锁定,并且在等待支付完成或者发生其他情况,导致该事务一直未提交(commit)或者回滚(rollback)。返回超时默认需要等待50秒,我们可以通过修改innodb_lock_wait_timeout参数来配置合理的等待时间。超时之后返回的错误如下:ERROR1205(HY000):Lockwaittimeoutexceeded;tryrestartingtransaction
情况3对用户来说,意味着卡死,完全不能接受。为什么会发生等待?在InnoDB的锁系统(locksystem)中,席位2如果被一个事务上了X(写锁)锁或者IX锁(意向更新锁),那么下一个事务要对席位2上X锁或者IX锁的事务,就要等待。这是由事务本身的特性(ACID)决定的。
那么是否有一种方法避免等待以及后续可能发生的超时呢?MySQL8.0提供的新功能SKIPLOCKED/NOWAIT就可以。SKIPLOCKED的意思是跳过那些已经被其他事务锁定了的席位。使用如下SKIPLOCKED语句进行席位锁定,那么返回的结果集可能为空,2或3,2和3。当结果集不为空时,返回的席位即被锁定成功。
SELECT*FROMseatsWHEREseat_noIN(2,3)ANDbooked='NO'FORUPDATESKIPLOCKED;
NOWAIT的意思是如果碰到被其他事务锁定的席位,不等待并直接返回错误。使用如下NOWAIT语句进行席位锁定,那么返回结果集2和3,要么返回错误。
SELECT*FROMseatsWHEREseat_noIN(2,3)ANDbooked='NO'FORUPDATENOWAIT;
如果返回错误,如下:
ERROR3572(HY000):Donotwaitforlock.
如果成功锁定两个席位,通过如下语句查询锁系统的状态:
SELECTthread_id,object_name,lock_type,lock_mode,lock_data,lock_statusFROMperformance_schema.data_locks;+-----------+-------------+-----------+-----------+-----------+-------------+|thread_id|object_name|lock_type|lock_mode|lock_data|lock_status| +-----------+-------------+-----------+-----------+-----------+-------------+|43|seats|TABLE|IX|NULL|GRANTED| |43|seats|RECORD|X|2|WAITING| |42|seats|TABLE|IX|NULL|GRANTED| |42|seats|RECORD|X|2|GRANTED| |42|seats|RECORD|X|3|GRANTED| +-----------+-------------+-----------+-----------+-----------+-------------+
SKIPLOCKED还可以很方便的用来进行随机分配席位。例如我们只需要锁定两个空的席位就可以通过如下语句实现。
SELECT*FROMseatsWHEREbooked='NO'LIMIT2FORUPDATESKIPLOCKED;
SKIPLOCKED/NOWAIT功能只针对行锁(recordlock),不包括表锁(tablelock),元数据锁(metadatalock/MDL)。因此,带有SKIPLOCKED/NOWAIT的查询语句依然可能会因为表锁或元数据库锁而阻塞。元数据锁是MySQLServer层用来保护数据库对象的并发访问的一致性而创建的,数据库对象不仅包括表,同时包括库,函数,存储过程,触发器,事件等等。表和行锁是InnoDB存储引擎内部为了保证事务的一致性而创建的不同粒度的锁。
另外,SKIPLOCKED/NOWAIT还可以配合FORSHARE使用,并且可以与单表绑定。例如:
SELECTseat_noFROMseatsJOINseat_rowsUSING(row_no)WHEREseat_noIN(2,3)ANDseat_rows.row_noIN(12)ANDbooked='NO'FORUPDATEOFseatsSKIPLOCKEDFORSHAREOFseat_rowsNOWAIT;
第二部分:SKIPLOCKED/NOWAIT在InnoDB中的代码实现
在InnoDB中,实现SKIPLOCKED/NOWAIT具体实现如下:
1.增加新的查询模式enumselect_mode{SELECT_ORDINARY=0,/*defaultbehaviour/SELECT_SKIP_LOCKED,/skiptherowifrowislocked/SELECT_NO_WAIT/returnimmediatelyifrowislocked*/};
2.在查询开始前,设置查询模式ha_innobase::store_lock():/*SetselectmodeforSKIPLOCKED/NO_WAIT*/switch(lock_type){caseTL_READ_SHARED_SKIP_LOCKED:caseTL_WRITE_SKIP_LOCKED:m_prebuilt->select_mode=SELECT_SKIP_LOCKED;break;caseTL_READ_SHARED_NO_WAIT:caseTL_WRITE_NO_WAIT:m_prebuilt->select_mode=SELECT_NO_WAIT;break;default:m_prebuilt->select_mode=SELECT_ORDINARY;break;}
3.上锁函数中,如果记录已被锁定,针对对不同查询模式进行相应处理:lock_rec_lock_slow():if(wait_for!=NULL){switch(sel_mode){caseSELECT_SKIP_LOCKED:err=DB_SKIP_LOCKED;break;caseSELECT_NO_WAIT:err=DB_LOCK_NOWAIT;break;
4.查询中对上锁结果进行处理:row_search_mvcc():caseDB_SKIP_LOCKED:gotonext_rec;对DB_LOCK_NOWAIT的处理则是回滚当前语句(statement),见函数row_mysql_handle_errors()。
5.二级索引(secondaryindex)的处理在InnoDB中,对表中记录的锁定分两种情况。第一种是查询使用是聚集索引(clusterindex),那么直接对聚集索引的记录上锁;第二中是查询使用的是二级索引,那么首先对二级索引的记录上锁,然后根据二级索引的记录,找到对应的聚集索引记录进行上锁。所以,对于第一部分订座的席位表中,如果存在二级索引,对于锁定表中一条记录而言,最终锁定成功与否,还是以锁定聚集索引记录为准。
SKIPLOCKED/NOWAIT可以非常高效地实现订座这个场景,作为InnoDB部分(WL#8919:InnoDB:ImplementNOWAITandSKIPLOCKED)的原作者,我也期待着大家来分享该功能更多的使用场景。
参考链接:
1.MySQL8.0.1:UsingSKIPLOCKEDandNOWAITtohandlehotrows
2.WL#3597:ImplementNOWAITandSKIPLOCKED
3.WL#8919:InnoDB:ImplementNOWAITandSKIPLOCKED
4.WL#6657:PERFORMANCE_SCHEMA,DATALOCKS
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。