- 使用Redisson第三方库
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
//使用这个interface中的API
private RedissonClient client;
- 自定义实现Redis做分布式锁
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis通过事件循环来处理网络I/O事件,能够同时处理多个客户端的连接请求,但其核心数据处理(读、写、删除)都是在一个单线程的主线程中处理,且每次只能执行一个命令,从而保证了数据的一致性。而分布式锁正是利用这一特性通过它的SETNX命令(键不存在则设置成功,表示获取锁)来实现简单的分布式锁功能。
获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, value);
try{
...具体业务
} finally {
释放锁
redisTemplate.delete(key);
}
缺陷:当业务程序未执行结束,出现宕机等情况导致未执行释放锁的操作,则会出现锁死的情况。
为防止锁死可以在获取锁时设置锁的过期时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, value, expire);
缺陷:setIfAbsent(key, value, expire)该方法执行的命令SETNX和EXPIRE命令是两个独立的操作,即先执行SETNX再执行EXPIRE,所以还是可能存在SETNX执行结束后出现服务宕机而导致锁死的情况。
针对上述情况只实现SETNX和EXPIRE命令一起作为一个原子性的操作执行即可。
- Lua实现SETNX和EXPIRE
1.使用Lua可以避免客户端与redis服务之间的多次通信减少网络开销,如当前SETNX和EXPIRE两个命令的执行就是两次网络连接请求。
2.Lua脚本与redis事务一起使用,要么全部成功要么全部失败,保证了Lua脚本中一系列命令的原子性操作
public <T> Boolean setIfAbsent(String key, T value, Duration duration) {
DefaultRedisScript<Boolean> luaScript = new DefaultRedisScript<>();
//获取lua脚本的资源文件
ClassPathResource resource = new ClassPathResource("setnx_expire.lua");
//setLocation和setScriptSource都可以,后者需要自己构建数据类型new ResourceScriptSource(resource)
luaScript.setLocation(resource);
luaScript.setResultType(Boolean.class);
//过期时间设置为秒
int expire = (int) duration.toSeconds();
return redisTemplate.execute(luaScript, Lists.newArrayList(key), value, expire);
}
Lua脚本:setnx_expire.lua
---获取第keys一个参数即键
local key = KEYS[1]
---获取args第一个参数的值即键对应的值
local value = ARGV[1]
---获取args第二个参数的值即键对应的过期时间,单位秒
local timeout = tonumber(ARGV[2])
---执行SETNX命令
local result = redis.call('SETNX', key, value )
if result == 1 then
--- 键不存在设置成功,并设置过期时间 EXPIRE
redis.call('EXPIRE', key, timeout )
return true
else
--- 键已存在设置失败
return false
end
上述Lua脚本中涉及到的通过KEYS和ARGV数组获取变量这个是在Redis中通过EVAL和EVALSHA执行lua脚本时的传参接收方式
redis.call()方法是redis服务在运行lua脚本时提供的一种接口用于访问redis数据
进入redis服务命令行,EVAL或EVALSHA执行lua脚本命令
redis-cli
> EVAL "local key = KEYS[1]; print(key)"
语法:
redis.call(command, key1, key2,..., arg1, arg2, ...)
命令描述 | redis-cli | lua script |
---|---|---|
获取键值 | GET key | local value = redis.call(‘GET’, ‘key’) |
设置键值 | SET key value | redis.call(‘SET’, ‘key’, ‘value’) |
键不存在则写如键值 | SET key value NX | redis.call(‘SETNX’, key ,value) |
删除键值 | DEL key [key…] | redis.call(‘DEL’, ‘key’) |
检查键值是否存在 | EXISTS key | redis.call(‘EXISTS’, ‘key’) |
获取键类型 | TYPE key | local type = redis.call(‘TYPE’, ‘key’) |
键值加1 | INCR key | redis.call(‘INCR’,‘key’) |
键值加指定增量 | INCRBY key increment | redis.call(‘INCRBY’,‘key’, number) |
键值减1 | DECR key | redis.call(‘DECR’,‘key’) |
键值减指定增量 | DECRBY key decrement | redis.call(‘DECRBY’,‘key’, number) |
获取一组键值 | MGET key [key…] | local values = redis.call(‘MGET’, ‘key1’, ‘key2’) |
哈希设置键值 | HSET hash hashkey value | redis.call(‘HSET’, ‘hash’, ‘hashkey’, ‘value’) |
集合添加一个或多个元素 | SADD key member [member …] | redis.call(‘SADD’, ‘set’, ‘member1’, ‘member2’) |
从集合移除元素 | SREM key member [member …] | redis.call(‘SREM’, ‘set’, ‘member1’, ‘member2’) |
设置键过期(秒) | EXPIRE key seconds | redis.call(‘EXPIRE’, ‘key’, seconds) |
设置键过期(毫秒) | PEXPIRE key milliseconds | redis.call(‘PEXPIRE’, ‘key’, milliseconds ) |
描述 | 其它命令 |
---|---|
哈希命令 | HGET / HGETALL / HDEL |
列表命令 | LPUSH / RPUSH / LPOP / RPOP |
集合命令 | SCARD / SISMEMBER / ZADD / ZREM / ZRANGE |