在分布式系统和高并发场景下,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 脚本用来只做库存的扣减,逻辑如下:
- 首先判度订单内的商品的库存是否足够
- 如果库存足够,扣减库存并返回剩余库存。
- 如果库存不足,则返回商品库存不足的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