携手创作,共同成长!这是我参与「掘金日新计划 · 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需要引入分布式锁。