【问题标题】:handling race conditions error prone code with sequelize使用 sequelize 处理竞争条件容易出错的代码
【发布时间】:2021-01-21 02:25:13
【问题描述】:

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

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

我想用代码维护三个聚合计数器:

  • spentMoney 和 purchaseCount(针对用户)
  • productCount(类别)

我知道聚合操作的简单查询、触发器、视图等选项。我只是想练习sequelize。

所以,我做到了,据我所知,它有效。 我的问题是关于方法:createProductregisterProductPurchase。那些容易出现竞争状况吗?我不这么认为,因为我使用的是版本号和事务。 在 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


    }

}

【问题讨论】:

  • 至少你不使用incrementProductCount中的事务。
  • @Anatoly 检查行:await category.incrementProductCount({ transaction: t })
  • 没错!你通过它但不要在incrementProductCountfunction()this.save()中使用它
  • @Anatoly 你是对的。完成。

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


【解决方案1】:

这行得通。我已经对其进行了测试,并且可以正常工作。您可以摆脱“购买”之类的自定义方法,并在服务类中处理该逻辑。但是,它会是一样的。 此外,如果需要,您可以为 OptimisticLockError 实现重试逻辑(但这不是最初的问题)

【讨论】:

    猜你喜欢
    • 2014-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-10
    • 1970-01-01
    • 2022-01-05
    • 1970-01-01
    • 2012-01-26
    相关资源
    最近更新 更多