V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lllei
V2EX  ›  数据库

数据库 race condition

  •  
  •   lllei · 2023-11-04 11:01:24 +08:00 · 1109 次点击
    这是一个创建于 386 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在写一个 fastapi 的后端,对 web 服务中的数据库的并发有些问题,希望大家解惑。

    参考了 官方教程 SQL-Database

    其中,添加用户的逻辑如下:

    @app.post("/users/", response_model=schemas.User)
    def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
        db_user = crud.get_user_by_email(db, email=user.email)
        if db_user:
            raise HTTPException(status_code=400, detail="Email already registered")
        return crud.create_user(db=db, user=user)
    

    其中 curd.create_user 实现如下:

    def create_user(db: Session, user: schemas.UserCreate):
        fake_hashed_password = user.password + "notreallyhashed"
        db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
        db.add(db_user)
        db.commit()
        db.refresh(db_user)
        return db_user
    

    我想问的是:

    这样不会有并发问题吗?我的意思是他是先检查一个用户邮箱是否存在,不存在则增加,那如果同时有两个呢?

    我测验结果是如果并发,会有两个同时加入。

    那么我应该如何通过什么手段解决这个问题呢?是在代码片段上加锁吗?那样不会拖慢速度吗?

    第 1 条附言  ·  2023-11-06 20:37:13 +08:00

    之前的问题已经解决,但还有一个问题是:如果修改分数和查询分数(几乎同时请求,但前者先),那么我应该保证查询分数是修改后的结果吗(即给查询加上共享锁),还是说不加锁(效率高,但结果是未知的)呢?

    10 条回复    2023-11-06 20:35:53 +08:00
    Flourite
        1
    Flourite  
       2023-11-04 11:09:04 +08:00
    要加锁,要么用 redis 要么用 db ,代码加锁?如果你有多个进程呢?
    0x19921213
        2
    0x19921213  
       2023-11-04 11:09:41 +08:00
    1. 设置 email 为唯一键
    2. 数据库乐观锁
    lllei
        3
    lllei  
    OP
       2023-11-04 11:09:49 +08:00
    我意识到以上问题可以通过 SQL 的 unique key 来解决。

    那么如果是现在的需求的让一道题的分数减少为原来的 $\dfrac{1}{10}$,又该怎么办呢?
    lllei
        4
    lllei  
    OP
       2023-11-04 11:10:37 +08:00
    @0x19921213 乐观锁吗,好,我去了解下:)
    lllei
        5
    lllei  
    OP
       2023-11-04 11:11:16 +08:00
    @Flourite hh ,我也觉得不现实,所以想来问一问需要用到什么知识
    kdd0063
        6
    kdd0063  
       2023-11-04 11:59:19 +08:00
    你这不就是 write skew 吗? write skew 如果在 RDBMS 的层面解决,必须要把隔离级别拉到 serializable 然后通过事务提交你的这个 CTA ( check then action ,本质上和 cas 等价)操作。如果在应用层面,也是类似的思路,只能通过独占锁或者独占调度来解决,如果你的后端应用是分布式部署的,还得上分布式锁。
    总之,幻读和 write skew 如果想完全杜绝,其实是对事务隔离和可见性提出了很高的要求,目前没有非常高效的解决方案。想要高性能就得牺牲一致性与可见性,至少目前这个桎梏在整个分布式数据系统上下文里还没有被突破。
    lllei
        7
    lllei  
    OP
       2023-11-04 12:18:22 +08:00
    @kdd0063 我刚在搜集资料时也看到类似的信息,发现我的疑问与数据库系统原理也有关,我现在去补补相关方面的知识再来理解下您的回答:)
    julyclyde
        8
    julyclyde  
       2023-11-04 12:42:48 +08:00
    你为什么要先检查再增加?
    try{直接增加} catch{已经存在}不就得了?

    甚至有些 orm 有 get or create 的功能
    wanziforever
        9
    wanziforever  
       2023-11-04 16:57:15 +08:00
    如果是 insert 的逻辑,那么直接 insert 就行了,然后检查返回消息是不是 duplicated 就行了,然后走你的用户重复的逻辑,不用先 select 一遍,然后再 insert ,这个操作从性能角度来说也不是那么好的。

    另外还可以用 upsert 之类的操作,但是对你你这种明显是创建用户的上下文,用 upsert 也不合适,的需要知道具体是不是已经存在了情况。
    lllei
        10
    lllei  
    OP
       2023-11-06 20:35:53 +08:00
    总结一下我这几天关于数据库的理解。

    首先题目这个问题可以通过 Unique Key 然后根据返回信息做即可。

    而我之后提到的让一道题的分数减少为原来的 $\dfrac{1}{10}$,则可以通过 SELECT ... FOR UPDATE 来解决,即在查询阶段加上行互斥锁,对于 MySQL 的 REPEATABLE READ 隔离模式下,行互斥锁在事务结束后才释放。值得一提的是,如果在没有 INDEX 的列上进行此操作会导致所有行 LOCK 。

    但还有一个问题是:如果修改分数和查询分数(几乎同时请求,但前者先),那么我应该保证查询分数是修改后的结果吗(即给查询加上共享锁),还是说不加锁(效率高,但结果是未知的)呢?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2737 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 11:22 · PVG 19:22 · LAX 03:22 · JFK 06:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.