【连载】redis库存操作,分布式锁的四种实现方式[四]--基于Redis lua脚本机制实现分布式锁
一、redis lua介绍
Redis 提供了非常丰富的指令集,但是用户依然不满足,希望可以自定义扩充若干指令来完成一些特定领域的问题。Redis 为这样的用户场景提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。
二、高并发情况下减库存的实现思路
由于lua脚本是原子性同步执行的,也就是说,我们可以将一堆操作封装为一个操作,让redis当做一条命令执行,这样,我们在分布式、高并发情况下,做减库存操作,每个客户端在执行操作时,其他客户端都是阻塞状态,相当于变相实现了分布式锁。
1、在本地缓存一份减库存的lua脚本,每次服务启动时,将脚本内容加载至内存;
2、请求处理时,会校验redis-server端是否存在该脚本,若存在,返回脚本的唯一id,客户端根据id调用脚本,并将参数传递过去执行
3、若redis-server端不存在该脚本,会先将脚本发送到server端缓存,返回id,进行调用
三、lua脚本的好处
1、减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延和请求次数。
2、原子性的操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
3、代码复用:客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本来完成相同的逻辑。
4、速度快:见 与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能; 对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。
5、可以移植:只要是有ANSI C 编译器的平台都可以编译,你可以看到它可以在几乎所有的平台上运行:从 Windows 到Linux,同样Mac平台也没问题, 再到移动平台、游戏主机,甚至浏览器也可以完美使用 (翻译成JavaScript).
6、源码小巧:20000行C代码,可以编译进182K的可执行文件,加载快,运行快。
四、代码实现
本地缓存一份减库存的lua脚本
local stockId = KEYS[1]; local decrNum = ARGV[1]; local result; print('key为', stockId); print('value为', decrNum); local crtStock = redis.call('get', stockId); print('当前库存为 :', crtStock); if crtStock == false or crtStock < decrNum then result = -2 else result = redis.call('decrBy', stockId, decrNum) end return result;
服务启动时,将脚本内容加载至内存,由静态字符串DECRBY_STOCK_SCRIPT接收
/** * 减库存脚本 */ private static String DECRBY_STOCK_SCRIPT = ""; /** * 初始化bean后,将加减库存的lua脚本加载至内存中 */ @PostConstruct public void loadLuaScript() { InputStream certStream = null; BufferedReader br = null; try { certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lua/decrByStock.lua"); br = new BufferedReader(new InputStreamReader(certStream, "UTF-8")); StringBuilder luaStr = new StringBuilder(); String line; while ((line = br.readLine()) != null) { luaStr.append(line).append(" "); } DECRBY_STOCK_SCRIPT = luaStr.toString(); LOGGER.info("减库存脚本初始化加载完毕,内容为:" + DECRBY_STOCK_SCRIPT); } catch (Exception e) { LOGGER.error("初始化库存管理Controller bean,加载操作库存脚本失败!" + e); } finally { if (certStream != null) { try { certStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
在服务启动时,会打印相应的日志
减库存逻辑代码
/** * 减库存(基于lua脚本实现) * * @param trace 请求流水 * @param stockManageReq(stockId、decrNum) * @return -1为失败,大于-1的正整数为减后的库存量,-2为库存不足无法减库存 */ @Override @ApiOperation(value = "减库存", notes = "减库存") @RequestMapping(value = "/decrByStock", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public int decrByStock(@RequestHeader(name = "Trace") String trace, @RequestBody StockManageReq stockManageReq) { long startTime = System.currentTimeMillis(); LOGGER.reqPrint(Log.CACHE_SIGN, Log.CACHE_REQUEST, trace, "decrByStock", JSON.toJSONString(stockManageReq)); int res = 0; String stockId = stockManageReq.getStockId(); Integer decrNum = stockManageReq.getDecrNum(); if (StringUtils.isBlank(DECRBY_STOCK_SCRIPT)) { LOGGER.error("减库存脚本为空!操作终止"); return -1; } LOGGER.info("减库存脚本内容为:" + DECRBY_STOCK_SCRIPT); try { if (null != stockId && null != decrNum) { stockId = PREFIX + stockId; // 加减库存lua脚本执行 Long result = (Long) this.evalshaScript(stockId, decrNum, DECRBY_STOCK_SCRIPT); LOGGER.info("脚本执行结果,result=" + result); res = result.intValue(); } } catch (Exception e) { LOGGER.error(trace, "decr sku stock failure.", e); res = -1; } finally { LOGGER.respPrint(Log.CACHE_SIGN, Log.CACHE_RESPONSE, trace, "decrByStock", System.currentTimeMillis() - startTime, String.valueOf(res)); } return res; } /** * 加减库存lua脚本执行 * * @param stockId 库存id * @param changeNum 加减库存的量 * @param script lua脚本 * @return 执行结果 */ private Object evalshaScript(String stockId, Integer changeNum, String script) { Object result = null; try (Jedis jedis = jedisPool.getWriteResource()) { if (jedis.select(0).equals("OK")) { // 将脚本缓存值redis server端,并返回脚本的唯一标识id String sha = jedis.scriptLoad(script); // 调用evalsha方法,执行脚本 result = jedis.evalsha(sha, 1, stockId, String.valueOf(changeNum)); } } return result; }
五、ab压测
5W请求,100并发,tps达到了4500,并且没有错误,相当强悍了
六、总结
lua脚本实现,可以保证正确性的同时,完全能够保证数据的一致性,可靠性方面就需要脚本的健壮性来保证,总之,效率比redisson、zk分布式锁要高太多,推荐使用