Redis + Lua
- 使用lua脚本的好处:
- 原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入,换句话说,编写脚本的过程中无需担心会出现插队、竞争等条件;
- 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行;
在Redis中使用Lua脚本
- 我们可以编写Lua脚本,然后再Lua脚本中调用Redis命令,使用redis.call函数调用。
- 比如调用string类型的命令:
- eval "return redis.call('set', 'k1', 'v1')" 0 // OK
- eval "return redis.call('get','k1')" 0
- eval "return redis.call('get', KEYS[1])" 1 K2 // 使用后置传参方式
127.0.0.1:6379> eval "return redis.call('set', 'name', 'zhangsan')" 0
OK
127.0.0.1:6379> eval "return redis.call('get', 'name')" 0
"zhangsan"
// jedis 使用方式
Jedis jedis = JedisPoolUtil.getInstance().getResource();
Object o = jedis.eval("return redis.call('get','name')");
EVALSHA命令
- 通过 script load "return redis.call('get','k1')" ,返回sha值:97a8c622c96716f456b9b29b31243e52eeafb1c2
- EVALSHA
97a8c622c96716f456b9b29b31243e52eeafb1c2 0
- 考虑到我们通过eval执行lua脚本,脚本比较长的情况下,每次调用脚本都需要把整个脚本传给redis,比较占用带宽,为了解决这个问题,redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本。
Jedis jedis = JedisPoolUtil.getInstance().getResource();
// 52da8c7de39385e305fb1af2a8ffd21534af996f
String s = jedis.scriptLoad("return redis.call('get','name')");
Object o = jedis.evalsha(s); // zhangsan
System.out.println("o = " + o);
Redis + Lua 实现分布式锁
- 依赖于redis里提供了
SETNX
互斥特性的命令
- SETNX:在Key不存在的情况下才会给 Key 设置值成功,否则返回0
- EXPIRE:设置过期时间,过期后自动删除key
- DEL :主动删除Key(释放锁)
- 主要使用以上3个命令实现分布式锁, 首先尝试使用 SETNX 设置值,如设置成功则等同于获取到锁,而其他线程不可能再设置成功,释放锁手动删除Key
- jedis 实现分布式锁
- lua 语言加锁
if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then
--设置成功返回1,当key不存在或者不能为key设置生存时间时,返回0
return redis.call('expire', KEYS[1], ARGV[2]);
else
--没有获取到锁
return 0;
end
- lua 语言解锁
if (redis.call('get', KEYS[1]) == ARGV[1]) then
--被删除key的数量
return redis.call('del', KEYS[1]);
else
return 0;
end
- jedis 代码实现
public class LuaJedisLock {
private String key;
/**
* 每一个锁的 key,不同的业务使用不同的key
*
* @param key
*/
public LuaJedisLock(String key) {
this.key = "redis:lock:" + key;
}
/**
* 获取锁
*
* @param timeout 获取锁等待的超时间
* @param processTime 处理过程中的超时时间
* @return
*/
public boolean acquireLock(long timeout, long processTime) {
Jedis jedis = null;
try {
jedis = JedisPoolUtil.getInstance().getResource();
// 使用SETNX 互斥命令 设置 key值,如果设置成功了,返回1
// 并且 设置超时时间, 为锁的最大处理时间, 设置成功也返回1
StringBuilder sb = new StringBuilder()
.append("if (redis.call('SETNX', KEYS[1], ARGV[1]) == 1) then ")
.append("return redis.call('expire', KEYS[1], ARGV[2]) ")
.append("else ")
.append("return 0 ")
.append("end ");
// 转换为 EVALSHA 命令执行
String sha1 = jedis.scriptLoad(sb.toString());
// 获取锁的最后时间, 否则则超时获取不到
long endTime = System.currentTimeMillis() + timeout;
// 当前时间 小于获取的超时时间,可以持续获取
while (System.currentTimeMillis() < endTime) {
Long result = (Long) jedis.evalsha(sha1, 1, key, "lockValue", String.valueOf(processTime));
if (result == 1) return true;
// 这里说明没有获取到锁,休眠一会,继续循环
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
if (jedis != null) {
jedis.close();
}
}
return false;
}
/**
* 释放锁, 就是删除 这个锁的key
*/
public void releaseLock() {
Jedis jedis = null;
try {
jedis = JedisPoolUtil.getInstance().getResource();
jedis.del(key);
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
分布式锁的不足
- 当加入分布式锁后,实际上就变成了单线程执行了,面对秒杀等较大QPS时,会很慢
- 可以采用,如将库存 分段 处理,不同的 段 使用 不同的分布式锁,使用不同的策略去访问不同段的数据,再加锁。
- 分段锁优化思想;
- 1000个库存,分成50段,每段20个库存;
- 随机或轮询算法定位分段;
- 支持同时50个并发下单;
- 假设每个下单耗时50ms;
- 50ms下单50;
- 1000ms下单=1000/50=20*50=1000个
Redis + Lua实现分布式限流
- 在请求访问时,先访问redis获取是否达到 当前单位时间内最大访问次数了
- 如果达到则直接返回,不再继续执行,如果没有 增加1访问次数,继续执行
- 是1秒内允许放行多少个请求,下一秒放行同样多的请求进来
public class LimitService {
/**
* 可以限制 指定的key 在一定时间内的 访问次数
* @param key
* @param expireTime
* @return
*/
public static Long limit2(String key, int expireTime) {
Jedis jedis = null;
try {
jedis = JedisPoolUtil.getInstance().getResource();
StringBuilder sb = new StringBuilder()
.append("local key1 = KEYS[1] ")
.append("local expireTime = ARGV[1] ")
.append("local count = redis.call('incr', key1) ")
.append("if (redis.call('ttl', key1) == -1) then ")
.append("redis.call('expire',key1, expireTime) ")
.append("end ")
.append("return count ");
String sha1 = jedis.scriptLoad(sb.toString());
Long count = (Long) jedis.evalsha(sha1, 1, key, String.valueOf(expireTime));
return count;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
- 使用方式
for(;;) {
Long cout = LimitService.limit2("duweiwu", 1);
if (cout <= 10) {
System.out.println("请求被处理...");
} else {
System.out.println("请求被限流, 对不起网络开小差了...");
}
Thread.sleep(50);
}