键自动过期功能,可以让特定的键在指定时间之后自动被移除。
流水线功能允许客户端把任意多条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开始引入,它带来了两大变化:
- 使得用户可以按需对Redis服务器增加新功能,而不必修改Redis的源码。
- 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时,服务器由两种反应:
- 正在运行的Lua脚本尚未执行过任何写命令,那么服务器将终止该脚本,然后回到正常状态,继续处理客户端的命令请求。
- 如果正在运行的Lua脚本已经执行过写命令,为了防止这些脏数据被保存到数据库中,服务器不会直接终止脚本并回到正常状态,用户只能使用SHUTDOWN nosave命令,手动重启服务器。