分类 Java 下的文章

近来遇到关于库存与并发的问题。由于一直接触的系统都是没有考虑过商品库存的并发,加上解决过的并发问题,也只是简单直接地采用锁表的方式。所以导致踩坑。

1 问题1,商品基础数据与库存数量,设计在同一个表。

商品基础数据,包括库存数量,主要用于查询。但库存数量,还要解决经常变化,且可能出现并发的情况。如果简单使用锁,即使只锁一行数据,也会导致正在进行下单(涉及扣减库存)的商品不能被浏览(因为锁住,不能查询)。

为了减轻这个情况,下单时,检查库存数量是否足够购买时,不锁数据,等到保存订单数据,真正扣减库存时才加锁。本想着通过减少锁数据的时间,减少商品数据不能查询的情况。但是系统采用Java开发,使用了Spring + Hibernate框架。而Hibernate在事务内使用了一级缓存,即事务内未提交时,查询到的业务数据都放到一级缓存。事务内查询时,会先查询一级缓存,若命中,则不再查询数据库。就导致了检查库存时已获取了商品数据,扣减库存时(从一级缓存获取)不能获取到最新库存(特别是两个客户同时下单同一个商品的情况),最后在并发情况下扣减库存,就出现库存扣少1了的问题。

解决方案很简单,把商品基础数据与库存数据分开两个表存放。库存数据在扣减时,不影响商品浏览。

2 问题2,库存数量,需减少锁定时间。

由于客户浏览商品,或者添加商品到购物车,都需要查询库存数据。如果使用悲观锁,即锁表或锁数据后不能查询,会导致客户不能浏览。参考了以下文章,决定使用乐观锁,即不使用数据库锁。

目前系统规模比较小,且没有涉及分布式,于是决定在扣减库存时直接更新数据的方式。即使用update语句扣减库存时,用where条件判断是否足够扣减,并返回是否扣减成功。

由于使用MySQL,update语句不能返回指定数据(但是,sql server可以使用update...output,PostgreSQL可用update...returning)。加上Hibernate不能同时执行update和select两个语句,最后采用存储过程。参考以下网址:

3 解决方案

总的来说,使用乐观锁(即没有使用数据库的锁),并利用MySQL存储过程实现扣减库存后返回结果。

1)库存表

create table `product_stock` (
    `productId` bigint not null comment '商品ID',
    `instock` int not null default '0' comment '库存数量',
    `createTime` datetime(3) default null comment '创建时间',
    `updateTime` datetime(3) default null comment '更新时间',
    primary key ( productId )
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_0900_ai_ci comment='商品库存';

2)扣减库存的存储过程

利用存储过程的out参数,返回扣减结果。当outUpdateQty返回的值大于零,扣减成功,否则失败。扣减成功,outStockAfter的值才是正确。

delimiter //
create procedure `product_reduce_instock`(
    in inProductId bigint, /*传入参数:商品ID*/
    in inReduceQty int, /*传入参数:扣减数量*/
    out outUpdateQty int, /*传出参数:实际扣减数量*/
    out outStockAfter int /*传出参数:更新后库存数量*/
)
begin
    -- 初始化返回的值
    set @updateQty=0;
    set @stockAfter=0;
    
    -- 执行扣减库存
    update product_stock 
    set instock = (@stockAfter := instock - (@updateQty := inReduceQty)), updateTime = now() 
    where productId = inProductId and instock >= inReduceQty;
    
    -- 传出参数赋值,即返回扣减结果
    set outUpdateStock=@updateQty;
    set outStockAfter=@updateQty;
end //

当Service方法被内部调用时,Spring注解会失效。就是Spring的Service类,如果public方法加上注解,类内部的其它方法使用this调用该方法,会导致注解失效。

例如Spring的Service实现类如下:

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = this.getUser(userId); // getUser方法的@Cacheable注解失效了
        return u == null ? "" : u.getName();
    }
}

原因是this引用的对象没有被Spring代理,调用该对象的public方法时,Spring不能处理相关注解。

解决方法很简单,就是使用Spring代理过的对象,代替this。然后只需解决如果获取该Service类被Spring代理过的对象。


1 循环依赖

就是自己注入自己。在Service类定义一个自身对象的属性,让Spring装配时把自己注入到自己。虽然Spring 5(具体版本待查证)声称解决了循环依赖的问题,但是Spring Boot 2.6.0开始默认设置不允许循环依赖。循环依赖是一个古老的问题,一样认为要避免。所以此方法不推荐

1)先设置

# Spring Boot 2.6.0之后,允许循环依赖
spring.main.allow-circular-references = true

2)上面的例子改为

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Autowired
    private UserService self; // 自己注入自己
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = self.getUser(userId); // 用self代替this,注解生效
        return u == null ? "" : u.getName();
    }
}

2 获取装配后的自己

避免Bean的循环依赖,主要思路是,在Bean装配完成后,再获取被Spring代理的自己。至于怎样获取,实现方法是多种多样的。

方法1,从Bean容器中获取自己。即:

UserService self = applicationContext.getBean("userService");

至于怎么获取Bean容器applicationContext,方法也是多样的。

方法2,开启AspectJ自动代理来获取自己。

详细参考:一个Spring AOP的坑!很多人都犯过!

要注意,AspectJ自动代理不只是解决本文档的问题,需考虑是否会带来未知的问题。

开启AspectJ自动代理的方法有多种,这里列出三种:

1)在启动类添加注解:

@EnableAspectJAutoProxy(proxyTargetClass=true, exposeProxy=true)

2)Spring增加配置:

<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true" />

3)Spring Boot的配置文件增加配置:

# 开启AspectJ自动代理
spring.aop.auto=true
# 开启CGLIB代理
spring.aop.proxy-target-class=true

然后就可以在当前Service类的方法中,通过类似的代码调用自身的方法,且能保证该方法的注解正常执行:

User u = ((UserService) AopContext.currentProxy()).getUser(userId);

方法3,延迟执行自己注入自己。

很简单,就是使用@Lazy注解,达到Bean初始化不执行自己注入自己,避免循环依赖的错误。我记得解决Spring 2.x循环依赖的问题时,也是采用延迟注入的配置。此方法写的代码最少,目前倾向采用这种方法。于是,上面的代码改为:

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Lazy
    @Autowired
    private UserService self; // 延迟自己注入自己
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = self.getUser(userId); // 用self代替this,注解生效
        return u == null ? "" : u.getName();
    }
}