聊聊解决库存超卖问题的5种锁方案

624 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情 >>


哈喽,大家好,我是一条。 本文基于单实例模式聊聊解决库存超卖问题的5种方案及其优劣。

环境搭建

jmeter.apache.org/download_jm… jmeter 下载地址

商品微服务,添加商品表

-- auto-generated definition
create table t_commerce_goods
(
    id                bigint auto_increment comment '自增主键'
        primary key,
    goods_category    varchar(64)   default ''      not null comment '商品类别',
    brand_category    varchar(64)   default ''      not null comment '品牌分类',
    goods_name        varchar(64)   default ''      not null comment '商品名称',
    goods_pic         varchar(256)  default ''      not null comment '商品图片',
    goods_description varchar(512)  default ''      not null comment '商品描述信息',
    goods_status      int           default 0       not null comment '商品状态',
    price             int           default 0       not null comment '商品价格',
    supply            bigint        default 0       not null comment '总供应量',
    inventory         bigint        default 0       not null comment '库存',
    goods_property    varchar(1024) default ''      not null comment '商品属性',
    create_time       datetime      default (now()) not null comment '创建时间',
    update_time       datetime      default (now()) not null comment '更新时间'
)
    comment '商品表' charset = utf8;
​
​

JVM 锁

不和数据库交互,模拟库存扣减。并发测试 100 个线程,访问50次。

private void jvmLock() {
    lock.lock();
    try {
        goods.setInventory(goods.getInventory()-1);
        log.info(goods.getInventory().toString());
    }finally {
        lock.unlock();
    }
}

不加锁出现并发问题,因为扣减和 set 不是原子操作,多个线程几乎同时拿到变量,多次扣减其实只减了一次。

用 voliate 能解决问题吗?不能,不保证原子性。

    private void mysqlLock() {
        CommerceGoods good = goodsMapper.selectOne(Wrappers.lambdaQuery(CommerceGoods.class)
                .eq(CommerceGoods::getGoodsName, "lock-test")
                .select()
        );
        good.setInventory(good.getInventory()-1);
        goodsMapper.updateById(good);
        log.info(good.getInventory().toString());
    }

毫无疑问,同样会出现超卖现象。加锁解决,这是肉眼可见的并发量和吞吐量下降。

for udpate 悲观锁

select * from table where name = productName for update;

直接修改

update table set inventory = (inventory - 1) where id = 1 ;

Redis 原子操作

不加锁就不会出现超卖,而且吞吐量很高。

    private void redisAtomic() {
        redisTemplate.opsForValue().decrement("lock-test");
        log.info(Objects.requireNonNull(redisTemplate.opsForValue().get("lock-test")).toString());
    }

CAS 乐观锁

update table set surplus = newQuantity where id = 1 and surplus = oldQuantity ;
    private void casLock() {
        int result = 0;
        while (result==0){
            CommerceGoods good = goodsMapper.selectOne(Wrappers.lambdaQuery(CommerceGoods.class)
                    .eq(CommerceGoods::getGoodsName, "lock-test")
                    .select());
            Long inventory = good.getInventory();
            good.setInventory(good.getInventory()-1);
            result = goodsMapper.update(good,Wrappers.lambdaUpdate(CommerceGoods.class)
                    .eq(CommerceGoods::getInventory,inventory));
        }
    }

本地锁失效

1.多例模式

synchronized 只锁当前对象,信息存放在对象头里。

spring 默认使用 jdk 的动态代理,spring boot 2.0 以后默认使用 cglid 动态代理。

@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)

2.事务

提交事务和执行锁的顺序:

1、开启事务(Aop)
2、加锁(进入synchronized方法)
3、释放锁(退出synchronized方法)
4、提交事务(Aop)

在可重复读隔离级别下,释放锁之后切换到另一个线程来读,无法读到未提交的事务。

解决:在 controller 层加锁。

3.多实例

只要不是在 Mysql 加的锁,都无法解决多实例的问题,如 jvm 锁,redis decre ,要解决多实例问题,就i需要引入分布式锁。