当前位置: 首页 > wzjs >正文

专业网站设计网站无线网络优化

专业网站设计网站,无线网络优化,济南直销网站制作,wordpress主题不显示小工具并发下正确的FirstOrCreate数据库操作 ​FirstOrCreate​是开发过程中经常能遇到的一种操作,即:尝试查找,如果不存在则创建。如此简单的操作,但是实际操作起来比较容易踩坑。 现有的FirstOrCreate是如何做的?有什么问…

并发下正确的FirstOrCreate数据库操作

FirstOrCreate​是开发过程中经常能遇到的一种操作,即:尝试查找,如果不存在则创建。如此简单的操作,但是实际操作起来比较容易踩坑。

现有的FirstOrCreate是如何做的?有什么问题?

在gorm框架中,有函数FirstOrCreate,但是我们可以发现其底层并不是用一句SQL实现的,而是拆分成了两句SQL,大概日志如下:

[INFO][2025-03-19T17:40:53.869+0800]_com_mysql_success||sql=SELECT * FROM `es_operator` WHERE `phone` = '123' ORDER BY `es_operator`.`id` LIMIT 1||proc_time=1.851||rows=0
[INFO][2025-03-19T17:40:53.872+0800]_com_mysql_success||sql=INSERT INTO `es_operator` (`phone`,`create_time`) VALUES ('123','0000-00-00 00:00:00')||proc_time=3.155||rows=1

这么操作的意思也很简单:尝试找到phone为123的数据,由于第一条结果为0,那就插入phone为123的数据。

对于并发比较敏感的同学应该很容易就可以看出来这样操作会在并发下存在重复插入数据的问题。

一个很直接的想法是Gorm框架为什么不将两步操作合并成一个事务中来解决并发的问题。

实际上是不能这么操作,因为简单的合并到一个事务中并不能解决并发下可能的重复插入问题,反而可能产生死锁的问题,具体看后文分解。

两步操作合并到一步可行吗?

由于索引会影响mysql的锁的范围,因此需要区分有无索引来分析。

  1. 数据库版本:5.7.16-log
  2. 模拟事务的操作:两个会话都执行如下SQL,在第一个执行之后执行第二个,第二个预期会在SELECT SLEEP(15);​的时候执行,从而完成达到如下图的时序。
START TRANSACTION;
SELECT * FROM es_operator WHERE phone = '123456678' FOR UPDATE;
SELECT SLEEP(15); //睡眠的目的是在第一个执行到这的时候等下第二个事务,从而可以方便测试后面Insert的情况
INSERT INTO es_operator (phone) VALUES ('123456678');
SELECT SLEEP(65);
COMMIT;

image

这里稍微解释下为什么SELECT​语句中有FOR UPDATE​,因为MySQL的隔离级别一般设置为可重复读,如果不加FOR UPDATE​,可重复读隔离级别下是用MVCC的,不会加锁,没有锁的话使用事务也没什么意义了。更具体的手动加锁见//对读取的记录加共享锁select ... lock in share mode;//对读取的记录加独占锁select ... for update;1

当然可以选择手动开启事务,然后一句话一句话执行,开始我没注意到可以这么操作,反应过来的时候已经开始写总结了,就偷懒不再来一遍了,因为最终结果都是一样的。

没有索引的情况

情况总结:由于没有索引,第一个事务的for update会直接开启表锁,因此第二个事务会直接等待第一个事务执行完之后才执行。 最后会导致数据库中有两条数据。

具体过程我们可以在事务执行过程中通过information_schema.INNODB_TRX​查看,可以看到另一个事务的状态(trx_state​)是LOCK WAIT​,在等待锁的释放。

mysql> SELECT * FROM information_schema.INNODB_TRX;
+----------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+----------------------------------------------------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+
| trx_id   | trx_state | trx_started         | trx_requested_lock_id | trx_wait_started    | trx_weight | trx_mysql_thread_id | trx_query                                                      | trx_operation_state | trx_tables_in_use | trx_tables_locked | trx_lock_structs | trx_lock_memory_bytes | trx_rows_locked | trx_rows_modified | trx_concurrency_tickets | trx_isolation_level | trx_unique_checks | trx_foreign_key_checks | trx_last_foreign_key_error | trx_adaptive_hash_latched | trx_adaptive_hash_timeout | trx_is_read_only | trx_autocommit_non_locking |
+----------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+----------------------------------------------------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+
| 38207662 | LOCK WAIT | 2025-04-19 15:34:30 | 38207662:1622197:3:5  | 2025-04-19 15:34:30 |          2 |                  32 | SELECT * FROM es_operator WHERE phone = '123456678' FOR UPDATE | starting index read |                 1 |                 1 |                2 |                  1136 |               1 |                 0 |                       0 | REPEATABLE READ     |                 1 |                      1 | NULL                       |                         0 |                         0 |                0 |                          0 |
| 38207661 | RUNNING   | 2025-04-19 15:34:27 | NULL                  | NULL                |          2 |                  31 | SELECT SLEEP(25)                                               | NULL                |                 0 |                 1 |                2 |                  1136 |               4 |                 0 |                       0 | REPEATABLE READ     |                 1 |                      1 | NULL                       |                         0 |                         0 |                0 |                          0 |
+----------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+----------------------------------------------------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+
2 rows in set (0.01 sec)

执行的时序图:

image

有索引的情况

有唯一索引的情况

有唯一索引的情况下,我们可以利用唯一索引冲突在SQL中直接完成并发安全的写法,like:

INSERT INTO es_operator (phone) VALUES ('123456678');
有普通索引的情况

情况总结:普通索引不同于没有索引,第一个事务的for update​不会锁表,而是用行锁+间隙锁的方式锁(独占),由于不存在此行,因此全部是间隙锁,间隙锁不冲突,因此第二个事务同样可以添加间隙锁。但是会在执行插入的时候死锁,导致一个事务rollback,另一个事务成功执行,最后数据库中有一条数据。

在事务执行过程中通过information_schema.INNODB_TRX​查看,可以看到两个事务的trx_state​都是RUNNING​。

mysql> SELECT * FROM information_schema.INNODB_TRX;
+----------+-----------+---------------------+-----------------------+------------------+------------+---------------------+------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+
| trx_id   | trx_state | trx_started         | trx_requested_lock_id | trx_wait_started | trx_weight | trx_mysql_thread_id | trx_query        | trx_operation_state | trx_tables_in_use | trx_tables_locked | trx_lock_structs | trx_lock_memory_bytes | trx_rows_locked | trx_rows_modified | trx_concurrency_tickets | trx_isolation_level | trx_unique_checks | trx_foreign_key_checks | trx_last_foreign_key_error | trx_adaptive_hash_latched | trx_adaptive_hash_timeout | trx_is_read_only | trx_autocommit_non_locking |
+----------+-----------+---------------------+-----------------------+------------------+------------+---------------------+------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+
| 38207684 | RUNNING   | 2025-04-19 18:02:08 | NULL                  | NULL             |          2 |                  32 | SELECT SLEEP(15) | NULL                |                 0 |                 1 |                2 |                  1136 |               1 |                 0 |                       0 | REPEATABLE READ     |                 1 |                      1 | NULL                       |                         0 |                         0 |                0 |                          0 |
| 38207683 | RUNNING   | 2025-04-19 18:02:06 | NULL                  | NULL             |          2 |                  31 | SELECT SLEEP(25) | NULL                |                 0 |                 1 |                2 |                  1136 |               1 |                 0 |                       0 | REPEATABLE READ     |                 1 |                      1 | NULL                       |                         0 |                         0 |                0 |                          0 |
+----------+-----------+---------------------+-----------------------+------------------+------------+---------------------+------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+
2 rows in set (0.01 sec)

执行过程中的navicate截图:报错1213,是死锁的报错:image

事务执行的时序图:

image

需要注意的是这里使用串行化隔离级别也是一样有这个问题的,原因在于使用串行化隔离级别相比于可重复读隔离级别,写锁是没什么区别的,只是读的时候也会加共享的读锁(MVCC的select是不加锁的)。

总结:我们应该怎么做?

FirstOrCreate​的语义上可以分析出来,语句插入的数据肯定是希望满足where条件的唯一性,比如说要求:用户uid唯一(单列唯一),一个用户每天只能创建一个帖子(多列组合唯一)。因此使用唯一索引或联合唯一索引冲突来解决这个问题。

如果实在不想使用唯一索引冲突,比如说脑袋抽抽了,或者考虑到唯一索引没办法利用change buffer来优化写入速度(意义不大) 或者因为涉及多个表甚至多个库导致无法用到唯一索引等情况,最好从业务侧来解决这个问题了,比如说:1.使用分布式锁,保证全局只有一个实例。

从未有经过严谨实战的角度来说,只推荐使用唯一索引的方式来解决问题

主要原因在于:

  1. 不出错,因为分布式锁还是有出错的可能性,因为场景都是数据严谨性要求特别高(一条重复都不能有)的场景。如果数据要求没那么严谨,也就没必要考虑重复的问题了,直接梭哈,有重复的再后期清理就对了。
  2. change buffer优化场景和能力有限且完全低效不了分布式锁带来的耗时:个人认为唯一索引的读取应该非常快且对于这种场景(先读后写),change buffer有多少优化效果也存疑。如果引入分布式锁,耗时反而上升!change buffer 详细见:09 普通索引和唯一索引,应该怎么选择?2

及其不推荐在能使用唯一索引或联合唯一索引的情况下使用select xxx for update​来解决这个问题:

  • 没有索引:会锁表 + 第二个事务也会执行成功,产生冗余数据!
  • 有索引:第二个事务会死锁检测而rollback,因此影响mysql性能+业务报错!

FirstOrCreate​遇到的问题和解决方案对于InsertOrUpdate​也是同样的适用。

参考:

mysql SERIALIZABLE隔离级别死锁问题 - 简书

ORM提供的FirstOrCreate方法,你用对了吗?-CSDN博客

创建联合唯一索引:MySQL中添加唯一约束和联合唯一约束_51CTO博客_mysql添加唯一约束


  1. //对读取的记录加共享锁
    select ... lock in share mode;//对读取的记录加独占锁
    select ... for update;
    
  2. 09 普通索引和唯一索引,应该怎么选择?

    结论先行:在字段不能重复的前提下,如果业务在插入前可以保证关键字段不重复,那么使用普通索引好于唯一索引

    首先介绍change buffer​,其也在innodb的buffer pool缓冲区中,其作用是延后写的时机:

    • 更新某一行数据:如果数据本身在buffer pool中,那么会直接更新该行数据,变成脏页。此时与change buffer 无关。
    • 更新某一行数据:如果数据不在buffer pool中,那么会直接更新change buffer,而不用读该行数据,就可以直接提交了。

    因此可以看到change buffer的作用就是在更新时内存中没有该行数据就可以避免读某行数据。
    该行数据会在:1.后台定期merge操作的时候更新;2.读取改行数据到内存的时候更新。

    根据change buffer​的作用总结:change buffer​ 的适用场合:写多读少 写了之后不会马上读。比如日志等功能。
    如果写了之后马上读,那么change buffer​ 无法节省时间空间,还会增加维护change buffer​ 的消耗。

    那么可以回到正题了,在业务可以保证唯一性的前提下,使用普通索引更好的原因是因为普通索引可以利用change buffer​,直接更新内存中的change buffer​和redo log​ 就可以提交了,而使用唯一索引,数据库就会检查唯一约束,此时会从数据库中读取数据(MySQL实际是按页来读取数据的),从而检查是否满足唯一约束,那么就无法利用change buffer加速了。

    change buffer​ 与 redo log优化思路的不同点:change buffer可以优化随机读,而redo log是用于优化随机写。

    这里面介绍了change buffer​,是小林中接近是一笔带过的内容,chang buffer​是buffer pool​的一部分,值得学习。 ↩

http://www.dtcms.com/wzjs/344287.html

相关文章:

  • 淘宝联盟的网站管理怎么做最新消息
  • 做电商怎么建网站seo技术服务外包
  • dede 网站地图模版宁波超值关键词优化
  • 《动态网站建设》在线测试千锋培训机构官网
  • 免费建建网站网站建设 网站制作
  • 商务邮箱注册优化营商环境建议
  • 美容行业网站建设多少价格下载百度到桌面上
  • 个人网站页面模板html以图搜图百度识图网页版
  • 怎么在网上免费做公司网站免费seo网站自动推广
  • 做网站是先做界面还是先做后台高清视频网络服务器
  • 常州在线制作网站网络推广文案怎么写
  • 手机网站开发 1433端口错误百度识图软件
  • 一品威客网是做啥的网站2023适合小学生的新闻事件
  • 邯郸做网站的电话百度客服电话号码
  • 网站域名备案时间查询seo公司运营
  • 网站建设子栏目怎么弄无锡百度关键词优化
  • 网站建设需要哪些方面佛山网站seo
  • 品牌形象设计案例网站广州seo工作
  • 西安做网站费用seo排名点击
  • 网页视频下载器app免费网站搜索引擎优化报告
  • 网站建设 中百度精准搜索
  • 云畅网站建设软文有哪些
  • 柳州网站建设22在哪里打广告效果最好
  • 4免费网站建站网络公司是做什么的
  • 哪个网站有免费互联网营销工具
  • 泉州市网站建设最近一周的新闻
  • 做网站要什么技术seo诊断报告怎么写
  • 焦作做网站推广网络营销是学什么
  • 泰和网站制作服务之家网站推广公司
  • 列出网站开发建设的步骤推广网址