Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本

发布于:2024-12-21 ⋅ 阅读:(12) ⋅ 点赞:(0)

键自动过期功能,可以让特定的键在指定时间之后自动被移除。

流水线功能允许客户端把任意多条Redis命令请求打包在一起,然后一次性将它们全部发送给服务器,服务器将这些命令都处理完毕后,会一次性地将它们的执行结果返回给客户端。

事务可以将多个命令打包成一个命令来执行,打包发送,打包执行。当事务成功执行时,事务中包含的所有命令都被执行,否则,所有命令都不执行。

Lua脚本是一个小巧的脚本语言,由C语言编写,目标是成为一个很容易嵌入其他语言中使用的语言。

1 自动过期

设置生存时间

EXPIRE key seconds

PEXPIRE key milliseconds

设置过期时间

EXPIREAT key timestamp

PEXPIREAT key milliseconds-timestamp

SET命令设置

SET key value [EX seconds | PX milliseconds | EXAT timestamp | PXAT milliseconds-timestamp]

获取剩余生存时间

(Time To Live,TTL)

TTL key

PTTL key

表 自动过期相关命令

1.1 示例

自动过期的登录会话:通过给会话令牌设置过期时间来让它在指定时间之后被移除,程序只需检查会话令牌是否存在,就能知道是否应该让用户重新登录了。

public class LoginSession {

    private final static Jedis jedis = RedisPool.getRedis();
    private final static String SESSION_KEY = "session_token_string::%s";
    private final static long DEFAULT_TIMEOUT = 2L;

    private static String generateToken() {
        return UUID.randomUUID().toString();
    }

    public static String saveToken(String userId) {
        String token = generateToken();
        jedis.setex(String.format(SESSION_KEY,userId),DEFAULT_TIMEOUT,token);
        return token;
    }

    public static void check(String userId,String token) {
        if(userId == null || token == null) throw new RuntimeException("用户id或token不能为空");
        String s = jedis.get(String.format(SESSION_KEY, userId));
        if (!token.equals(s)) throw new RuntimeException("token已失效");
        System.out.println("确认成功");
    }

    public static void main(String[] args) throws InterruptedException {
        String token = saveToken("123");
        Thread.sleep(1000);
        check("123",token);
        Thread.sleep(1500);
        check("123",token);
    }
}

2 流水线与事务

流水线可以显著降低网络通信次数,大幅度减少程序在网络通信方面耗费的时间,使得程序的执行效率得到显著的提示。

但是流水线不能保证命令被全部执行,而事务可以,事务打包发送,打包执行。

2.1 流水线

Redis不会限制客户端在流水线中包含的命令数量,却会为客户端的输入缓存区设置默认值为1GB的上限。当客户端发送的数据量超出这一限制时,Redis将强制关闭该客户端。

2.2 事务

开启事务

MULTI

执行事务

EXEC

放弃事务

DISCARD

表 事务相关命令

2.2.1 乐观锁

WATCH key [key ...]

对一个或多个键进行监控,如果客户端在尝试执行事务之前(MULTI之后,EXEC之前),这些键值发送了变化,那服务器将拒绝执行客户端发送的事务,并向它返回一个空值。

当事务执行完成后,会自动取消对这些键的监控。

UNWATCH [key ...]

取消对键值的监控,当没有指定键时,会取消对所有键的监控。

2.3 示例

带身份验证功能的锁:在获取锁时,直到删除这个锁的这段时间,锁键的值可能会发生变化,使用乐观锁区保证DEL命令只会在锁键的值没有发生任何变化的情况下执行。

public class IdentityLock {

    private final static String LOCK_KEY = "identity_lock_string";
    private final static Long DEFAULT_TIMEOUT = 5L;

    private final static Date START_TIME = new Date();

    public static boolean acquire(String user) {
        Jedis jedis = RedisPool.getRedis();
        SetParams setParams = new SetParams();
        setParams.ex(DEFAULT_TIMEOUT);
        setParams.nx();
        String set = jedis.set(LOCK_KEY, user, setParams);
        RedisPool.getRedisPool().returnResource(jedis);
        return "OK".equals(set);
    }

    public static void release(String user) {
        Jedis jedis = RedisPool.getRedis();
        jedis.watch(LOCK_KEY);
        String lock = jedis.get(LOCK_KEY);
        Transaction transaction = jedis.multi();
        try {
            if (user.equals(lock)) {
                transaction.del(LOCK_KEY);
                System.out.println(user + "释放锁");
                transaction.exec();
            } else {
                transaction.discard();
            }
        } catch (Exception e) {
            transaction.discard();
        } finally {
            transaction.close();
            jedis.unwatch();
            RedisPool.getRedisPool().returnResource(jedis);
        }
    }

    private static class UserThread extends Thread {
        private final String userId;
        private final int timeout;
        private UserThread(String userId, int timeout) {
            this.userId = userId;
            this.timeout = timeout;
        }
        @Override
        public void run() {
            System.out.println(userId + "尝试获取锁:" + timeout);
            while (true) {
                if (acquire(userId)) {
                    Date date = new Date();
                    long second = (date.getTime() - START_TIME.getTime()) / 1000;
                    System.out.println(userId + "获取锁成功:" + second + "s");
                    break;
                }
            }
            try {
                Thread.sleep(timeout * 1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            release(userId);
        }
    }

    public static void main(String[] args) {
        RedisPool.getRedis().del(LOCK_KEY);
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            new UserThread(i+ "",random.nextInt(7) + 1).start();
        }
    }
}

3 Lua 脚本

Lua脚本从Redis2.6开始引入,它带来了两大变化:

  1. 使得用户可以按需对Redis服务器增加新功能,而不必修改Redis的源码。
  2. Redis服务器以原子方式执行Lua脚本。

3.1 执行脚本

EVAL script num_keys key [key ...] arg [arg ...]

script: 是脚本本身

num_keys: 脚本需要处理的键数量,后面跟的key 为键名,数量与num_keys一致。

arg: 指定传递给脚本的附加参数。

Lua 的数组起始索引是1.

图 EVAL 使用示例

3.1.1 使用脚本执行Redis命令

在脚本中调用redis.call()函数或redis.pcall()函数。这两个函数的参数相同(参数如上面示例所示),不同点在于处理错误的方式。前者在执行命令出错时会引发一个Lua错误,迫使EVAL命令向调用者返回一个错误;而后者会将错误包裹起来,并返回一个表示错误的Lua表格。

图 call与pcall处理错误的方式

3.1.2 值转换

带有小数部分的Lua数字将被转换为Redis整数回复,要想向Redis返回小数,着需要在Lua中,将这个小数转换为字符串。

图 Lua向Redis输出小数

3.2 缓存并执行脚本

在定义脚本后,程序通常会重复执行脚本,客户端每次执行脚本都需要将相同的脚本重新发送一次,会很浪费网络带宽。

SCRIPT LOAD script

将给定的脚本缓存在服务器中,并返回对应的SHA1校验和

EVALSHA sha1 num_keys key [key ...] arg [arg ...]

执行已被缓存的脚本。

图 缓存并执行脚本示例

3.2.1 脚本管理

检查脚本是否已被缓存

SCRIPT EXISTS sha1 [sha1 ...]

移除所有已缓存脚本

SCRIPT FLUSH [ASYNC | SYNC]

强制停止正在运行的脚本

SCRIPT KILL

表 脚本管理相关命令

使用KILL时,服务器由两种反应:

  1. 正在运行的Lua脚本尚未执行过任何写命令,那么服务器将终止该脚本,然后回到正常状态,继续处理客户端的命令请求。
  2. 如果正在运行的Lua脚本已经执行过写命令,为了防止这些脏数据被保存到数据库中,服务器不会直接终止脚本并回到正常状态,用户只能使用SHUTDOWN nosave命令,手动重启服务器。