库存问题优化

库存问题优化

Scroll Down

场景描述

ERP项目开发中,多用户同时操作出库时,可能存在超发的情况。

功能流程

用户在进行出库操作时,会先获取库存数据,判断是否有足够库存出库。如果有继续,如果没有则提示。

问题

如果A,B同时提交请求,A,B用户同时判断通过,就容易造成库存量为负数的情况。

方案A

乐观锁方式
乐观锁方式实现基于版本号。即在数据表中增加version字段,每次读取库存数量时都会读取到version版本号。在执行更新时进行判断,如果版本号一直,那么版本号+1并修改库存数据。否则就不修改。跳出异常。(版本号+1 确保不出现ABA的情况)
代码示例

        SysTest sysTest = sysTestMapper.selectTest(1L);
        Integer version = sysTest.getVersion();
        int i = sysTestMapper.updateInfo(1, version);
        if (i == 0) 
	//查询
	select xx from sys_test where id = #{id}
	//修改	
        update sys_test set version = version + 1 , stock = stock - #{stock} where id = #{id} and version = #{version}

测试

这里的测试我将使用jmeter进行并发测试。
这里将设置线程数和循环次数。并且设置好请求头的token认证信息。增加结果集子模块便于我们查看请求状态。
image.png
请求均成功
image.png
查看idea的控制台情况。为了便于观察,我将成功或者失败结果输出 在控制台上。
image.png
image.png
可以看到还是有很多失败的情况。这样就可以避免了。

新问题

如果每次失败后都会跳出操作失败请重试,这样用户体验就会很差,所以针对这个问题,要想办法解决掉。

解决A

增加提交失败重试

do {
            // 查询最新版本

            // 更新
	    
	   // 判断是否更新成功
            if (success) {
                return true;
            } else {
	  // 重试次数判断
                Number++;
                //这里的次数可以在类中获取配置文件中属性。@Value("${xxx.xxx}")
                if (Number == 10) {
                    log.error("超过最大重试次数");
                    break;
                }
            try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    log.error(e);
                }				
            }
        } while ();

通过设置循环次数以及线程休眠后重试,提高用户体验程度。

解决B

通过注解方式,这里使用spring提供的@Retryable注解进行重试。
1、POM文件引入

	<dependency>
		<groupId>org.springframework.retry</groupId>
		<artifactId>spring-retry</artifactId>
	</dependency>

2、应用启动类增加注解,开启重试

@EnableRetry

3、在计算库存方法上增加注解

@Retryable(XXX)

@Retryable的参数说明:

value:抛出指定异常才会重试
include:和value一样,默认为空,当exclude也为空时,默认所以异常
exclude:指定不处理的异常
maxAttempts:最大重试次数,默认3次
backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为1秒,第二次为3秒,第三次为4.5秒。

4、去掉以上方案的do while逻辑,增加throw抛出异常,注解设置接收所有异常。

解决B遇到的问题

因为我的计算库存方法和调用计算库存方法都在一个实现类里,导致有@Retryable注解的计算库存方法无法调用,查找资料后发现,这是因为由于retry用到了aspect增强,如果方法内部调用,会使aspect增强失效,那么retry当然也会失效。所以在同一个类内无法调用@Retryable注解方法。
解决
单独写个service,进行调用。但是感觉这样比较复杂,代码会因为这种实现比较乱,这种方案弃用。

方案B

悲观锁实现方式
for update锁行,修改数据后释放锁。确保数据不会超发。
前提:需要在查询和更新方法上增加@Transactionl事务注解
数据库Innodb

select xx from xxwhere id = xx for update 
 update xx set xx where id=xx 

需注意
for update语句必须有明确的字段过滤,并且本字段需要是主键或者有索引的字段。否则会出现锁表的情况。

对比总结

两种方案对比:
乐观锁:乐观锁实际是一个无锁的情况,如果需要多次重试,去操作数据库,将加大系统的整体吞吐量。
悲观锁:没有乐观锁重试环节,如果系统并发较多,需要多次修改数据,悲观锁是不错的选择。

选择:
本系统的实际应用场景对库存数据的修改并不集中且不多,对于读多写少的情况,我优先选择了乐观锁去实现ERP系统的库存计算方面的问题。