zoukankan      html  css  js  c++  java
  • 【连载】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分布式锁要高太多,推荐使用 

  • 相关阅读:
    hdu 6702 ^&^ 位运算
    hdu 6709 Fishing Master 贪心
    hdu 6704 K-th occurrence 二分 ST表 后缀数组 主席树
    hdu 1423 Greatest Common Increasing Subsequence 最长公共上升子序列 LCIS
    hdu 5909 Tree Cutting FWT
    luogu P1588 丢失的牛 宽搜
    luogu P1003 铺地毯
    luogu P1104 生日
    luogu P1094 纪念品分组
    luogu P1093 奖学金
  • 原文地址:https://www.cnblogs.com/ft535535/p/10151169.html
Copyright © 2011-2022 走看看