【连载】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分布式锁要高太多,推荐使用 

 

posted @ 2018-12-20 18:10  李军军  阅读(1402)  评论(0编辑  收藏  举报