关于在spring boot中使用lua脚本结合的讨论

在分布式系统和高并发场景下,Redis 是一种非常流行的缓存和数据库解决方案。而在某些复杂的业务场景中,单一的 Redis 命令无法满足我们对原子性和效率的需求。为了处理多步操作或确保操作的原子性,Lua 脚本可以作为一种强大的工具,与 Redis 一起使用,能够在服务端一次性完成多个操作。

在 Spring Boot 中,借助 RedisTemplate 组件,我们可以轻松集成并执行 Lua 脚本。这种结合不仅提升了代码的灵活性,还确保了操作的原子性,尤其是在诸如库存管理、限流操作等高并发场景中,能够大大减少竞争条件带来的问题。

本文将讨论如何在 Spring Boot 中通过 RedisTemplate 执行 Lua 脚本,逐步展示如何将 Lua 与 Redis 结合应用于真实业务场景。接下来,我们将通过具体代码示例,介绍如何在 Redis 中编写 Lua 脚本,并通过 Spring Boot 项目将其加载和执行。

为什么选择 Lua 脚本?

Redis 提供了丰富的命令用于操作数据,但在一些需要多个命令顺序执行且保证原子性操作的场景中,Lua 脚本显得尤为重要。其主要优势包括:

  • 原子性:所有 Redis 命令在 Lua 脚本中执行时是原子操作,保证了数据一致性。
  • 减少网络开销:Lua 脚本在服务端运行,一次请求可执行多条命令,减少了客户端与服务端之间的通信。
  • 灵活性:Lua 脚本可以处理复杂逻辑,如条件判断、循环等,而这些逻辑在单条 Redis 命令中是无法实现的。

接下来,我们将通过一个示例展示如何在 Spring Boot 应用中集成 Lua 脚本,利用 JSON 数据进行数据传输,并通过 Redis 来实现库存扣减的操作。这种方法不仅高效,而且可以确保操作的原子性。

Lua 脚本示例

首先,我们编写一个简易 Lua 脚本用来只做库存的扣减,逻辑如下:

  1. 首先判度订单内的商品的库存是否足够
  2. 如果库存足够,扣减库存并返回剩余库存。
  3. 如果库存不足,则返回商品库存不足的result直接返回给前台

库存扣减 Lua 脚本( lua``/addsub_stock_amount_list.lua ):

lua 复制代码
local redis_key = 'stock::detail'
local redis_hget = function(key)
    return redis.call('hget', redis_key, key)
end
local redis_hincrby = function(key, value)
    return redis.call('hincrby', redis_key, key, value)
end

local success = { --定义返回体
    result = true
}
local error = { --定义返回体
    result = false,
    code = 203
}

local pay_stock_array = cjson.decode(KEYS[1])
local message = KEYS[3]



for _, stock in ipairs(pay_stock_array) do --进行预扣减 检查订单内所有商品的库存是否充足
    local id = stock.id --商品的规格id
    local stock_amount = tonumber(stock.stockAmount) -- 要扣减的库存数量
    local goods_spec_name = stock.goodsSpecName --规格名称
    local goods_name = stock.goodsName --商品名称
    if redis_hget(id) + stock_amount < 0 then --这里的stock_amount是负数,因为是要减库存。这样做好处是,加库存和减库存可以用一套代码。
        error['msg'] = string.format('商品:%s(%s),%s', goods_name, goods_spec_name, message)
        return cjson.encode(error)
    end
end

for _, stock in ipairs(pay_stock_array) do --进行正式的扣减并计算扣减后的库存与扣减前的库存 方便做库存日志
    stock['stockAmountBefore'] = redis_hget(stock.id)
    stock['stockAmountAfter'] = redis_hincrby(stock.id, tonumber(stock.stockAmount))
end
success['object'] = pay_stock_array --包装返回提
return cjson.encode(success) --返回结果

Spring Boot 中的 Lua 脚本执行

为了高效地在 Spring Boot 中执行 Lua 脚本,我们可以利用 RedisTemplate 来实现。通过将 Lua 脚本注册到 Spring 的 IOC 容器中,我们可以避免每次调用时重复加载脚本。这里,我们将使用 BeanDefinitionRegistryPostProcessor 接口来将 resource资源文件中的Lua 脚本注入到容器中,从而实现更高效的使用。

java 复制代码
@Configuration
@Slf4j
public class RedisScriptConfig implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {

    }

    @Override
    @SneakyThrows
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resourcePatternResolver.getResources("/lua/*.lua");
        for (Resource resource : resources) {
//            log.info("加载lua脚本:{}", resource.getFilename());
            String beanName = resource.getFilename().split("\.")[0];
            DefaultRedisScript redisScript = new DefaultRedisScript();
            redisScript.setScriptSource(new ResourceScriptSource(resource));
            redisScript.setResultType(String.class);
            beanFactory.registerSingleton(beanName, redisScript);
        }
    }
}

最后我们在需要使用lua脚本的地方通过@Autowired注入获取redisScript利用RedisTemplate的execute方法执行RedisScript

java 复制代码
@Service
public class StockExportServiceImpl {
    @Autowired
    private RedisScript addsub_stock_amount_list; //这里使用的是lua脚本的文件名。
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private StockExportDetailMapper stockExportDetailMapper;

    public Result exportStock(StockExportEntity stockExportEntity) {
        //获取出库明细
        List<StockExportDetailEntity> list = stockExportDetailMapper.exportDetailList(stockExportEntity.getExportCode());
        List<RedisStockEntity> redisStock = list.stream().map(e -> new RedisStockEntity().setId(e.getGoodsSpecId()).setStockAmount(e.getAmount().negate()).setGoodsName(e.getGoodsName()).setGoodsSpecName(e.getGoodsSpecName())).collect(Collectors.toList());
        String redisResult = (String) stringRedisTemplate.execute(addsub_stock_amount_list, Arrays.asList(JSONObject.toJSONString(redisStock), null, "库存不足无法出库"));
        Result<JSONArray> result = JSONObject.parseObject(redisResult, Result.class);
        if (!result.getResult()) {
            return result;
        }
        //更新数据库存
        //插入库存日志
        //修改订单状态
        return new Result();
    }
}

StockExportServiceImpl 中,我们从数据库获取库存出库明细,并将其转换为 RedisStockEntity 。然后,通过 RedisTemplate 执行 Lua 脚本进行库存扣减,并处理返回结果。

尽管这个示例展示了基本的库存扣减功能,但仍然有许多扩展的可能性。例如:

  • 如何处理促销活动中的商品库存。
  • 如何在库存管理中涵盖入库、库存上限、出库和退款等复杂场景。

但不要在 Lua 脚本中编写过多的业务逻辑,因为一旦脚本出现问题,可能会导致 Redis 的崩溃,进而影响整个系统的稳定性。应将复杂的业务逻辑保留在 Java 代码中,而将简单的原子操作委托给 Lua 脚本,以确保代码的清晰和可维护性。

希望本文能够为您在 Spring Boot 中集成 Lua 脚本提供一些有价值的见解,帮助您在处理高并发和复杂业务场景时提升系统的效率和可靠性。

参考文档:
Spring Data Redis Documentation
Redis Lua API Documentation

相关推荐
XMYX-05 小时前
Spring Boot + Prometheus 实现应用监控(基于 Actuator 和 Micrometer)
spring boot·后端·prometheus
@yanyu6666 小时前
springboot实现查询学生
java·spring boot·后端
酷爱码7 小时前
Spring Boot项目中JSON解析库的深度解析与应用实践
spring boot·后端·json
java干货8 小时前
虚拟线程与消息队列:Spring Boot 3.5 中异步架构的演进与选择
spring boot·后端·架构
武昌库里写JAVA10 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
小白杨树树11 小时前
【WebSocket】SpringBoot项目中使用WebSocket
spring boot·websocket·网络协议
clk660717 小时前
Spring Boot
java·spring boot·后端
爱敲代码的TOM18 小时前
基于JWT+SpringSecurity整合一个单点认证授权机制
spring boot