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

数据库 race condition

  •  
  •   lllei · 176 天前 · 930 次点击
    这是一个创建于 176 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在写一个 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 条附言  ·  173 天前

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

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

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

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

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

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

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

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