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

  • 相关阅读:
    slf4j的简单用法以及与log4j的区别
    [转]Git 代码撤销、回滚到任意版本(当误提代码到本地或master分支时)
    【转】IDEA 中配置文件properties文件中文乱码解决
    Python+Selenium练习篇之3-浏览器滚动条操作
    selenium操作下拉滚动条的几种方法
    python利用unittest进行测试用例执行的几种方式
    安装和使用 Python
    PM2实用入门指南
    linux清除缓存
    【centos6.6环境搭建】Github unable to access SSL connect error出错处理
  • 原文地址:https://www.cnblogs.com/ft535535/p/10151169.html
Copyright © 2011-2022 走看看