sql - 使用 sequelize 处理竞争条件容易出错的代码

标签 sql node.js sequelize.js race-condition optimistic-locking

我正在尝试创建一个非常小的项目,以便我可以练习 Sequelize 。
基本上我有用户、产品、类别和购买。

  • 用户最多可以购买 3 个产品
  • 用户不能多次购买相同的产品。
  • 每个产品属于一个类别

  • 我想用代码维护三个聚合计数器:
  • 花费的金钱和购买次数(对于用户)
  • productCount(用于类别)

  • 我知道聚合操作的简单查询、触发器、 View 等选项。我只是想练习 Sequelize 。
    所以,我做到了,据我所知,它有效。
    我的问题是关于方法: createProduct registerProductPurchase 。那些容易出现竞争条件吗?我不这么认为,因为我使用的是版本号和事务。
    createProduct 中,我正在检查类别的版本号,在 registerProductPurchase 中检查用户的版本号。它们都在事务开始时加载。
    createProduct 映射到此 sql:
    Executing (9136e456-63f0-4753-98eb-a28099b0d881): START TRANSACTION;
    Executing (9136e456-63f0-4753-98eb-a28099b0d881): SELECT "id", "name", "productCount", "version" FROM "categories" AS "ProductCategory" WHERE "ProductCategory"."id" = 2;
    Executing (9136e456-63f0-4753-98eb-a28099b0d881): INSERT INTO "products" ("id","name","price","categoryId") VALUES (DEFAULT,$1,$2,$3) RETURNING "id","name","price","categoryId";
    Executing (default): UPDATE "categories" SET "productCount"=$1,"version"=$2 WHERE "id" = $3 AND "version" = $4
    Executing (9136e456-63f0-4753-98eb-a28099b0d881): COMMIT;
    
    和 registerProductPurchase 映射到这个:
    Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): START TRANSACTION;
    Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): SELECT "id", "name", "price", "categoryId" FROM "products" AS "Product" WHERE "Product"."id" = 2;
    Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): SELECT "id", "email", "spentMoney", "purchaseCount", "version" FROM "users" AS "User" WHERE "User"."id" = 2;
    Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): SELECT "purchaseDate", "userId", "productId" FROM "purchases" AS "Purchase" WHERE "Purchase"."productId" = 2 AND "Purchase"."userId" IN (2);
    Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): INSERT INTO "purchases" ("userId","productId") VALUES (2,2) RETURNING "purchaseDate","userId","productId";
    Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): UPDATE "users" SET "spentMoney"=$1,"purchaseCount"=$2,"version"=$3 WHERE "id" = $4 AND "version" = $5
    Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): COMMIT;
    
    
    这对我来说似乎很好。
    这是代码(相关部分。我也在使用 express )
    如果我的代码中存在竞争条件,我该如何测试?
    var sequelize = require("sequelize")
    
    const db = new sequelize.Sequelize('postgres://root:root@localhost:5432/catalog')
    
    var Category = db.define('ProductCategory', {
    
        id: {
            type: sequelize.DataTypes.INTEGER,
            autoIncrement: true,
            primaryKey: true
    
        },
        name: {
            type: sequelize.DataTypes.STRING,
            allowNull: false
        },
        productCount: {
            type: sequelize.DataTypes.INTEGER,
            allowNull: true,
            defaultValue: 0
        }
    
    }, {
        version: true,
        timestamps: false,
        tableName: 'categories',
        modelName: 'ProductCategory'
    });
    
    var Product = db.define('Product', {
    
        id: {
            type: sequelize.DataTypes.INTEGER,
            autoIncrement: true,
            primaryKey: true
    
        },
        name: {
            type: sequelize.DataTypes.STRING,
            allowNull: false
    
        },
        price: {
            type: sequelize.DataTypes.DOUBLE,
            allowNull: false
    
        }
    }, {
        timestamps: false,
        tableName: 'products',
        modelName: 'Product'
    });
    
    var User = db.define('User', {
    
        id: {
            type: sequelize.DataTypes.INTEGER,
            autoIncrement: true,
            primaryKey: true
    
        },
        email: {
            type: sequelize.DataTypes.STRING,
            allowNull: false,
            unique: true
        },
        spentMoney: {
            type: sequelize.DataTypes.DOUBLE,
            allowNull: false,
            defaultValue: 0
        },
        purchaseCount: {
            type: sequelize.DataTypes.INTEGER,
            allowNull: false,
            defaultValue: 0
        }
    }, {
        version: true,
        timestamps: false,
        tableName: 'users',
        modelName: 'User'
    });
    
    var Purchase = db.define('Purchase', {
        purchaseDate: {
            type: sequelize.DataTypes.DATEONLY,
            allowNull: true
        }
    }, {
        version: false,
        timestamps: false,
        tableName: 'purchases',
        modelName: 'Purchase'
    });
    
    Category.hasMany(Product, { foreignKey: "categoryId" })
    Product.belongsTo(Category, { foreignKey: "categoryId" })
    
    Product.belongsToMany(User, { through: Purchase, foreignKey: "userId" })
    User.belongsToMany(Product, { through: Purchase, foreignKey: "productId" })
    
    
    Category.prototype.incrementProductCount = async function(options) {
    
        this.productCount += 1
    
        return await this.save(options)
    
    }
    
    User.prototype.buy = async function(product, options) {
    
        if (this.purchaseCount === 3)
            throw new Error("User can't buy more than 3 products")
    
        try {
    
            await this.addProduct(product, options)
    
        } catch (err) {
    
            if (err instanceof UniqueConstraintError) {
                throw new Error("User already bought product " + p.id)
            }
    
            throw err
    
        }
    
        this.spentMoney += product.price
        this.purchaseCount += 1
    
        await this.save(options)
    
    
    }
    
    async function createProduct(categoryId, productInfo) {
    
        let t
    
        try {
    
            t = await db.transaction()
    
            let category = await Category.findByPk(categoryId, { transaction: t })
    
            let product = await Product
                .build({...productInfo, categoryId: categoryId })
                .save({ transaction: t })
    
            await category.incrementProductCount({ transaction: t })
    
            await t.commit()
    
            return product
    
        } catch (error) {
    
            if (t)
                await t.rollback()
    
            throw err
    
    
        }
    
    }
    
    async function registerProductPurchase(userId, productId) {
    
        let t
    
        try {
    
            t = await db.transaction()
    
            let product = await Product.findByPk(productId, { transaction: t })
    
            let user = await User.findByPk(userId, { transaction: t })
    
            await user.buy(product, { transaction: t })
    
            await t.commit()
    
        } catch (error) {
    
            if (t)
                await t.rollback()
    
            throw error
    
    
        }
    
    }
    
    

    最佳答案

    这有效。我已经测试过它并且它有效。您可以摆脱诸如“购买”之类的自定义方法并在服务类内部处理该逻辑。但是,它会是一样的。
    此外,如果需要,您可以为 OptimisticLockError 实现重试逻辑(但这不是最初的问题)

    关于sql - 使用 sequelize 处理竞争条件容易出错的代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65782628/

    相关文章:

    mysql - 如何在数据集中生成值

    mysql - 我的查询没有使用我的索引,我如何使用解释计划并用 MySQL 修复这个缓慢的查询

    node.js - ZMQ 发布/订阅订阅

    javascript - Sequelize 更新不再起作用 : "Missing where attribute in the options parameter passed to update"

    postgresql - 在表空间中创建表 sequelize

    mysql - 如何使用 WHERE 子句显示 Null 结果

    sql - 在 postgres 中跨多个列聚合函数

    javascript - Preact CLI 如何访问索引页面

    javascript - Node-fetch 返回 Promise { <pending> } 而不是所需的数据

    node.js - 使用sequelize 比较查询中日期的当前年份和月份