zoukankan      html  css  js  c++  java
  • Redis Lua实战

    1.1 EVAL script numkeys key [key ...] arg [arg ...] 

    numkeys 是key的个数,后边接着写key1 key2...  val1 val2....,举例

    127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 val1 val2
    1) "key1"
    2) "key2"
    3) "val1"
    4) "val2"

     

    1.2 SCRIPT LOAD script 

    把脚本加载到脚本缓存中,返回SHA1校验和。但不会立马执行,举例

    127.0.0.1:6379> SCRIPT LOAD "return 'hello world'"
    "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"

    1.3 EVALSHA sha1 numkeys key [key ...] arg [arg ...] 

    根据缓存码执行脚本内容。举例

    复制代码
    127.0.0.1:6379> SCRIPT LOAD "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 
    "a42059b356c875f0717db19a51f6aaca9ae659ea"
    127.0.0.1:6379> EVALSHA "a42059b356c875f0717db19a51f6aaca9ae659ea" 2 key1 key2 val1 val2
    1) "key1"
    2) "key2"
    3) "val1"
    4) "val2"
    复制代码

    1.4 SCRIPT EXISTS script [script ...] 

    通过sha1校验和判断脚本是否在缓存中

    1.5 SCRIPT FLUSH 

    清空缓存

    复制代码
    127.0.0.1:6379> SCRIPT LOAD "return 'hello jihite'"
    "3a43944275256411df941bdb76737e71412946fd"
    127.0.0.1:6379> SCRIPT EXISTS "3a43944275256411df941bdb76737e71412946fd"
    1) (integer) 1
    127.0.0.1:6379> SCRIPT FLUSH
    OK
    127.0.0.1:6379> SCRIPT EXISTS "3a43944275256411df941bdb76737e71412946fd"
    1) (integer) 0
    复制代码

    1.6 SCRIPT KILL 

    杀死目前正在执行的脚本

    2. 主要优势

    减少网络开销:多个请求通过脚本一次发送,减少网络延迟

    原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务

    复用:客户端发送的脚本永久存在redis中,其他客户端可以复用脚本

    可嵌入性:可嵌入JAVA,C#等多种编程语言,支持不同操作系统跨平台交互

    3. 实战

    直接在redis-cli中直接写lua脚本,这样非常不方便编辑,通常情况下我们都是把lua script放到一个lua文件中,然后执行这个lua脚本,

    示例:活跃用户判断:判断一个游戏用户是否属于活跃用户,如果符合标准,则活跃用户人数+1

    if redis.call("EXISTS",KEYS[1]) == 1 then
         return redis.call("INCRBY",KEYS[1],ARGV[1])
       else
         return nil
       end

    存储位置:

    /Users/jihite/activeuser.lua

    执行

    复制代码
    $ redis-cli --eval /Users/jihite/activeuser.lua user , 1
    (integer) 1
    
    127.0.0.1:6379> get user
    "1"
    127.0.0.1:6379> exit
    $ redis-cli --eval /Users/jihite/activeuser.lua user , 1
    (integer) 2
    $ redis-cli 
    127.0.0.1:6379> get user
    "2"
    127.0.0.1:6379> exit
    $ redis-cli --eval /Users/jihite/activeuser.lua user , 4
    (integer) 6
    复制代码
     

    4. 脚本的安全性

    如生成随机数这一命令,如果在master上执行完后,再在slave上执行会不一样,这就破坏了主从节点的一致性

    为了解决这个问题, Redis 对 Lua 环境所能执行的脚本做了一个严格的限制 —— 所有脚本都必须是无副作用的纯函数(pure function)。所有刚才说的那种情况压根不存在。Redis 对 Lua 环境做了一些列相应的措施:

    • 不提供访问系统状态状态的库(比如系统时间库)
    • 禁止使用 loadfile 函数
    • 如果脚本在执行带有随机性质的命令(比如 RANDOMKEY ),或者带有副作用的命令(比如 TIME )之后,试图执行一个写入命令(比如 SET ),那么 Redis 将阻止这个脚本继续运行,并返回一个错误。
    • 如果脚本执行了带有随机性质的读命令(比如 SMEMBERS ),那么在脚本的输出返回给 Redis 之前,会先被执行一个自动的字典序排序,从而确保输出结果是有序的。
    • 用 Redis 自己定义的随机生成函数,替换 Lua 环境中 math 表原有的 math.random 函数和 math.randomseed 函数,新的函数具有这样的性质:每次执行 Lua 脚本时,除非显式地调用 math.randomseed ,否则 math.random 生成的伪随机数序列总是相同的。

    5. Redis Lua脚本与事务

    从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。

    使用事务时可能会遇上以下两种错误:

    • 事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
    • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。

    对于发生在 EXEC 执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。

    不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。

    在 Redis 2.6.5 以前, Redis 只执行事务中那些入队成功的命令,而忽略那些入队失败的命令。 而新的处理方式则使得在流水线(pipeline)中包含事务变得简单,因为发送事务和读取事务的回复都只需要和服务器进行一次通讯。

    至于那些在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。

    经过测试lua中发生异常处理方式和redis 事务一致,可以说这两个东西是一样的,但是lua支持缓存,可以复用脚本,这个是原来的事务所没有的

    6. spring-data-redis操作lua

    上面讲的是如何在redis控制台调用lua脚本,现在我们来讲下怎么在java里面调用
    在java里面调用redis一般使用jedis,对于调用lua脚本来讲,spring-data-redis包做的封装使用起来更加方便,底层也是基于jiedis,所以我们这边直接讲spring-data-redis中的redisTemplate如何来调用lua
    先导入依赖

    <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
                    <version>1.8.1.RELEASE</version>
    </dependency>
    然后我们使用StringRedisTemplate这个类来操作
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    public <T> T runLua(String fileClasspath, Class<T> returnType, List<String> keys, Object ... values){
            DefaultRedisScript<T> redisScript =new DefaultRedisScript<>();
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(fileClasspath)));
            redisScript.setResultType(returnType);
            return stringRedisTemplate.execute(redisScript,keys,values);
        }
    这个框架把lua脚本封装成RedisScript对象,并且可以将lua脚本执行的结果自动转换为配置的java类型,然后只要直接调用execute方法即可

    并且这个execute逻辑中封装了evalsha的优化,源码如下

    protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType, int numKeys,
                byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) {
    
            Object result;
            try {
                result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs);
            } catch (Exception e) {
    
                if (!exceptionContainsNoScriptError(e)) {
                    throw e instanceof RuntimeException ? (RuntimeException) e : new RedisSystemException(e.getMessage(), e);
                }
    
                result = connection.eval(scriptBytes(script), returnType, numKeys, keysAndArgs);
            }
    
            if (script.getResultType() == null) {
                return null;
            }
    
            return deserializeResult(resultSerializer, result);
        }
    因为sha1的算法是通用的,所以在java客户端可以提前算出SHA1校验和,然后用evalsha来执行脚本,如果SHA1对应的脚本,那么还是用eval来执行,eval执行一次后,下次都可以直接调用evalsha了,减少网络开销

    7. lua Debug

    我们写完一个lua脚本,lua和redis的数据类型是不一致的,存在一个转换,并且如果遇到复杂逻辑的lua脚本,如果不能debug,只在自己脑子里面走这个逻辑,是不科学的,如果redis lua也提供了debug功能,要在redis客户端执行
    在运行lua的eval,加上-ldb即可开启debug功能,debug只支持eval命令

    ./redis-cli --ldb --eval /tmp/script.lua mykey somekey , arg1 arg2

    然后提供了一些调试命令

    lua debugger> help
    Redis Lua debugger help:
    [h]elp               Show this help.
    [s]tep               Run current line and stop again.
    [n]ext               Alias for step.
    [c]continue          Run till next breakpoint.
    [l]list              List source code around current line.
    [l]list [line]       List source code around [line].
                         line = 0 means: current position.
    [l]list [line] [ctx] In this form [ctx] specifies how many lines
                         to show before/after [line].
    [w]hole              List all source code. Alias for 'list 1 1000000'.
    [p]rint              Show all the local variables.
    [p]rint <var>        Show the value of the specified variable.
                         Can also show global vars KEYS and ARGV.
    [b]reak              Show all breakpoints.
    [b]reak <line>       Add a breakpoint to the specified line.
    [b]reak -<line>      Remove breakpoint from the specified line.
    [b]reak 0            Remove all breakpoints.
    [t]race              Show a backtrace.
    [e]eval <code>       Execute some Lua code (in a different callframe).
    [r]edis <cmd>        Execute a Redis command.
    [m]axlen [len]       Trim logged Redis replies and Lua var dumps to len.
                         Specifying zero as <len> means unlimited.
    [a]abort             Stop the execution of the script. In sync
                         mode dataset changes will be retained.
    
    Debugger functions you can call from Lua scripts:
    redis.debug()        Produce logs in the debugger console.
    redis.breakpoint()   Stop execution as if there was a breakpoint in the
                         next line of code.
    用redis.debug() 可以打日志

    用redis.breakpoint()在lua脚本里打断点
    s和n都是跳到下行代码
    c是跳到下个断点
    list可以展示当前这条代码前后的代码

    写个简单的lua脚本来测试下

    local value1 = ARGV[1]
    local value2 = ARGV[2]
    redis.debug(value1)
    redis.debug(value2)
    if(value1>value2)
    then
    return "a"
    else
    return "b"
    end

    更多细节看官方教程

    8.项目实战

    在我们项目中使用redis生成全局id,代码如下

    @Autowired
        private RedisTemplate<String,Long> redisTemplate;
    
    
        public  String nextID(){
            String key = Prefix+simpleDateFormatThreadLocal.get().format(new Date());
            Long existedID = redisTemplate.opsForValue().get(key);
            if(existedID!=null){
                redisTemplate.opsForValue().set(key,existedID+1);
                return key+String.format("%04d",existedID+1);
            }else{
                redisTemplate.opsForValue().set(key,1L);
                return key+"0001";
            }
        }
    这段代码是存在问题的,在并发的情况下,get方法可以访问到相同的key,就会出现id重复的问题,测试代码如下
     
    System.out.println("current:"+idGenerator.currentID());
            Integer threadSize =5;
            final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    for(int i =0 ;i<100;i++){
                        System.out.println(Thread.currentThread().getName()+":"+idGenerator.nextID());
                    }
                    countDownLatch.countDown();
                }
            };
            for(int i =0;i<threadSize;i++){
                new Thread(runnable,"Thread"+i).start();
            }
            countDownLatch.await();
            System.out.println("current:"+idGenerator.currentID());

    当然这边我们也可以使用乐观锁或者分布式锁来实现,但是锁自旋的逻辑还是有潜在危险的
    如果用lua来实现,把这个阻塞动作放在redis服务器,那我们的代码就会很健壮了
    新建一个lua脚本

    local key = KEYS[1]
    local id = redis.call('get',key)
    if(id == false)
    then
        redis.call('set',key,1)
        return key.."0001"
    else
        redis.call('set',key,id+1)
        return key..string.format('%04d',id + 1)
    end

    对应调用java代码如下

    public String nextIDLua(){
            String key = Prefix+simpleDateFormatThreadLocal.get().format(new Date());
            DefaultRedisScript<String> redisScript =new DefaultRedisScript<>();
            redisScript.setLocation(new ClassPathResource("lua/genID.lua"));
            redisScript.setResultType(String.class);
            //System.out.println(redisScript.getSha1());
            return redisTemplate.execute(redisScript,(RedisSerializer<?>) redisTemplate.getKeySerializer(),(RedisSerializer<String>)redisTemplate.getKeySerializer(),Lists.newArrayList(key));
        }
  • 相关阅读:
    js正则表达式大全(2)
    Magic Trackpad 2 on win10 x64
    Google 日历短信通知没有了
    Ueditor 1.4.3 jsp utf-8版Bug修复
    [转]eclipse中build workspace的相关优化
    Hello,
    EpCloud开发日志
    为服务创建安装程序
    winform 通过WCF上传Dataset数据
    opcrcw.da.dll 和.net 4.0
  • 原文地址:https://www.cnblogs.com/junzi2099/p/14208796.html
Copyright © 2011-2022 走看看