Redis客户端
通信协议
Redis监听默认6379的端口号,可以通过TCP方式建立连接。
服务端约定了一种特殊的消息格式,每个命令都是以rn(CRLF回车+换行)结尾。这种编码格式我们之前在AOF文件里面见到了,叫做Redis Serialization Protocol(RESP,Redis序列化协议),发消息或者响应消息需要按这种格式编码,接收消息需要按这种格式解码。Redis设计这种格式的原因:容易实现、解析快、可读性强。Redis6.0新特性里面说的RESP协议升级到了3.0版本,其实就是对于服务端和客户端可以接收的消息进行了升级扩展,比如客户端缓存的功能就是在这个版本里面实现的。
我们来看下这种编码格式实际内容是什么。使用wireshark对jedis抓包:
执行一个set请求(set qingshan 2673):
可以看到实际发出的数据包是:
再执行一个get请求(get qingshan),抓包:
实际内容是:
其实就是把他们的长度命令、命令和参数用\r\n连接起来。
我们也可以自己实现一个Redis的Java客户端。
- 建立Socket连接
- OutputStream写入数据(发送命令到服务端)
- InputStream读取数据(从服务端接收数据)
public class MyClient {
private Socket socket;
private OutputStream write;
private InputStream read;
public MyClient(String host, int port) throws IOException {
socket = new Socket(host, port);
write = socket.getOutputStream();
read = socket.getInputStream();
}
/**
* 实现了set方法
* @param key
* @param val
* @throws IOException
*/
public void set(String key, String val) throws IOException {
StringBuffer sb = new StringBuffer();
// 代表3个参数(set key value)
sb.append("*3").append("\r\n");
// 第一个参数(set)的长度
sb.append("$3").append("\r\n");
// 第一个参数的内容
sb.append("SET").append("\r\n");
// 第二个参数key的长度(不定,动态获取)
sb.append("$").append(key.getBytes().length).append("\r\n");
// 第二个参数key的内容
sb.append(key).append("\r\n");
// 第三个参数value的长度(不定,动态获取)
sb.append("$").append(val.getBytes().length).append("\r\n");
// 第三个参数value的内容
sb.append(val).append("\r\n");
// 发送命令
write.write(sb.toString().getBytes());
byte[] bytes = new byte[1024];
// 接收响应
read.read(bytes);
System.out.println("-------------set-------------");
System.out.println(new String(bytes));
}
/**
* 实现了get方法
* @param key
* @throws IOException
*/
public void get(String key) throws IOException {
StringBuffer sb = new StringBuffer();
// 代表2个参数
sb.append("*2").append("\r\n");
// 第一个参数(get)的长度
sb.append("$3").append("\r\n");
// 第一个参数的内容
sb.append("GET").append("\r\n");
// 第二个参数key的长度
sb.append("$").append(key.getBytes().length).append("\r\n");
// 第二个参数内容
sb.append(key).append("\r\n");
write.write(sb.toString().getBytes());
byte[] bytes = new byte[1024];
read.read(bytes);
System.out.println("-------------get-------------");
System.out.println(new String(bytes));
}
public static void main(String[] args) throws IOException {
MyClient client = new MyClient("192.168.44.181", 6379);
client.set("shihui", "2673");
client.get("shihui");
}
}
使用这种协议,我们可以用Java实现所有的Redis操作命令。
官网推荐的Java客户端有3个:Jedis,Redisson和Luttuce.
Spring操作Redis提供了一个模板方法,RedisTemplate。这并不是Spring实现了一个Redis的客户端,实际上是Spring定义了一个连接工厂接口:RedisConnectionFactory。这个接口有很多实现,例如:JedisConnectionFactory、JredisConnectionFactory、LettuceConnectionFactory、SrpConnectionFactory。也就是说,RedisTemplate对其他现成的客户端再进行了一层封装而已。在Spring Boot 2.x版本之前,RedisTemplate默认使用Jedis,2.x版本之后,默认使用Lettuce.。
Jedis
https://github.com/redis/jedis/
如果不用RedisTemplate,就可以直接创建Jedis的连接。
public class BasicTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("这个参数是ip地址", 6379);
jedis.set("shihui", "2673jedis");
System.out.println(jedis.get("shihui"));
jedis.close();
}
}
Jedis有一个问题:多个线程使用一个连接的时候线程不安全。
解决思路是:
使用连接池,为每个请求创建不同的连接,基于Apache common pool实现。Jedis的连接池有三个实现:JedisPool,ShardedJedisPool,JedisSentinelPool,都是用getResource从连接池获取一个连接。
public class JedisPoolTest {
public static void main(String[] args) {
ordinaryPool();
shardedPool();
sentinelPool();
}
/**
* 普通连接池
*/
public static void ordinaryPool(){
JedisPool pool = new JedisPool("这个参数是ip地址",6379);
Jedis jedis = pool.getResource();
jedis.set("shihui","石灰");
System.out.println(jedis.get("shihui"));
}
/**
* 分片连接池
*/
public static void shardedPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// Redis服务器
JedisShardInfo shardInfo1 = new JedisShardInfo("这个参数是ip地址", 6379);
// 连接池
List<JedisShardInfo> infoList = Arrays.asList(shardInfo1);
ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);
ShardedJedis jedis = jedisPool.getResource();
jedis.set("shihui","分片测试");
System.out.println(jedis.get("shihui"));
}
/**
* 哨兵连接池
*/
public static void sentinelPool() {
String masterName = "redis-master";
Set<String> sentinels = new HashSet<String>();
sentinels.add("这个参数是ip地址1:26379");
sentinels.add("这个参数是ip地址2:26379");
sentinels.add("这个参数是ip地址3:26379");
JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
pool.getResource().set("shihui", "哨兵" + System.currentTimeMillis() + "石灰");
System.out.println(pool.getResource().get("shihui"));
}
}
Jedis的功能比较完善,Redis官方的特性全部支持,比如发布订阅、事务、Lua脚本、客户端分片、哨兵、集群、pipeline等等。
Sentinel和Cluster的功能我们已经知道了。Jedis连接Sentinel需要配置所有的哨兵地址。Cluster连接哨兵只需要配置任何一个master或者slave的地址就可以了。
Sentinel获取连接原理
Sentinel是如何返回最新可用的Master地址的呢?
源码:
在构造方法中(JedisSentinelPool 95行)
pool = new JedisSentinelPool(masterName, sentinels);
调用了:
HostAndPort master = initSentinels(sentinels, masterName);
查看:initSentinels方法
private HostAndPort initSentinels(Set<String> sentinels,final String masterName){
HostAndPort master = null;
boolean sentinelAvailable = false;
log.info("Trying to find master from available Sentinels...");
//有多个sentinels,遍历这些个sentinels
for(String sentinel:sentinels){
//host:port表示的sentinel地址转化为一个HostAndPort对象。
final HostAndPort hap = HostAndPort.parseString(sentinel);
log.fine("Connecting to Sentinel"+ hap);
Jedis jedis = null;
try{
//连接到sentinel
jedis = new Jedis(hap.getHost),hap.getPort():
//根据masterName得到master的地址,返回一个list,host=list[O],port =//list[1]
List<String> masterAddr =jedis.sentinelGetMasterAddrByName(masterName);
//connected to sentinel.
sentinelAvailable = true;
if(masterAddr-= null || masterAddr.size()!= 2){
log.warning("Can not get master addr,master name:"+ masterName +".Sentinel:"+ hap
+".");
continue;
}
//如果在任何一个sentinel中找到了master,不再遍历sentinels
master = toHostAndPort(masterAddr);
log.fine("Found Redis master at"+ master);
break;
}catch(JedisException e){
//resolves#1036,it should handle JedisException there's another chance
//of raising JedisDataException
log.warning("Cannot get master address from sentinel running@"+hap+".Reason:"+ e +".Trying next one.");
}finally{
if(jedis != null){
jedis.close();
}
}
}
//到这里,如果master为null,则说明有两种情况,一种是所有的sentinels节点都down掉了,一种是master节点没有被存活的sentinels监控到
if(master ==null){
if(sentinelAvailable){
//can connect to sentinel,but master name seems to not
/monitored
throw new JedisException("Can connect to sentinel,but"+ masterName
+"seems to be not monitored...");
}else{
throw new JedisConnectionException("All sentinels down,cannot determine
where is" + masterName+"master is running..");
}
}
//如果走到这里,说明找到了master的地址
log info("Redis master running at"+ master +",starting Sentinel listeners..");
//启动对每个sentinels的监听为每个sentinel都启动了一个监听者MasterListener。
//MasterListener本身是一个线程,它会去订阅sentinel上关于master节点地址改变的消息。
for(String sentinel:sentinels){
final HostAndPort hap = HostAndPort.parseString(sentinel);
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
//whether MasterListener threads are alive or not,process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
Cluster获取连接原理
使用Jedis连接Cluster的时候,我们只需要连接到任意一个或者多个redis group中的实例地址。
那我们是怎么获取到需要操作的Redis Master实例的?
为了避免get,set的时候发生重定向错误,我们需要把slot和Redis节点的关系保存起来,在本地计算slot,就可以获得Redis节点信息。
如何存储slot和Redis连接池的关系?
-
程序启动初始化集群环境,读取配置文件中的节点配置,无论是主从,无论多少个,只拿第一个,获取redis连接实例。
//redis.clients.jedis.JedisClusterConnectionHandler#initializeslotsCache private void initializeSlotsCache(Set<HostAndPort startNodes,GenericObjectPoolConfig poolConfig,String password){ for(HostAndPort hostAndPort:startNodes){ //获取一个Jedis实例 Jedis jedis = new Jedis(hostAndPort.getHost(),hostAndPort.getPort()); if(password!= null){ jedis.auth(password); } try{ //获取Redis节点和Slot虚拟槽 cache.discoverClusterNodesAndSlots(jedis); //直接跳出循环 break; }catch(JedisConnectionException e){ //try next nodes finally{ }finally{ if(jedis != null){ jedis.close(); } } } }
-
discoverClusterNodesAndSlots方法,用获取的redis连接实例执行clusterSlots()方法,实际执行redis服务端cluster slots命令,获取虚拟槽信息。该集合的基本信息为[long,long,List,List],第一,二个元素是该节点负责槽点的起始位置,第三个元素是主节点信息,第四个元素为主节点对应的从节点信息。该list的基本信息为[string,int,string],.第一个为host信息,第二个为port信息,第三个为唯id。
-
获取有关节点的槽点信息后,调用getAssignedSlotArray(slotinfo)来获取所有的槽点值。
-
再获取主节点的地址信息,调用generateHostAndPort(hostInfo)方法,生成一个hostAndPort对象。
-
在assignSlotsToNode方法中,再根据节点地址信息来设置节点对应的JedisPool,即设置Map<String,JedisPool> nodes的值。
-
接下来判断若此时节点信息为主节点信息时,则调用assignSlotsToNodes方法,设置每个槽点值对应的连接池(slave不需要连接),即设置Map<Integer,JedisPool> slots的值。
//redis clients.jedis.JedisClisterlnfoCache#discowerClusterNodesAndSlots public void discowerClusterNodesAndslots(Jedis jedis){ w.lock(); try{ reset(); //获取节点集合 List<Object> slots = jedis.clusterSlots(); //遍历3个master节点 for(Object slotInfoObj : slots){ //slotInfo槽开始,槽结束,主,从 //{[0,5460,7291,7294],[5461,10922,7292,7295],[10923,16383,7293,7296]} List<Object> slotInfo =(List<Object)slotlnfoObj //如果<=2,代表没有分配slot if(slotInfo.size()<= MASTER_NODE_INDEX){ continue; } //获取分配到当前master节点的数据槽,例如7291节点的{0,1,2.3....5460} List<Integer> slotNums = getAssignedSlotArray(slotInfo); //hostinfos int size = slotInfo.size(); //size是4,槽最小最大,主,从 //第3位和第4位是主从端口的信息 for(int i = MASTER_NODE_INDEX; i<size; i++){ List<Object> hostInfos =(List <Object>)slotInfo.get(i); if(hostInfos.size()<=0){ continue; } //根据IP端口生成HostAndPort实例 HostAndPort targetNode = generateHostAndPort(hostInfos); //据HostAndPort解析出ip:port的key值,再根据key从缓存中查询对应的jedisPool实例。如果没有jedisPool实例,就创建JedisPool实例,最后放入缓存中。 //nodekey和nodePool的关系 setupNodeIfNotExist(targetNode); //把slot和jedisPool缓存起来(16384个),key是slot下标,value是连接池 if(i==MASTER_NODE_INDEX){ assignSlotsToNode(slotNums,targetNode); } } } }finally{ w.unlock(); } }
很明显,这个Map有16384个key,key对应的value是一个连接池信息。有几个Redis Group(或者说有几个master),就有几个不同的连接池。
获取slot和Redis实例的对应关系之后,接下来就是从集群环境存取值。
Jedis集群模式下所有的命令都要调用这个方法:核心代码:
JedisClusterCommand#runWithRetries 116行
connection = connectionHandler. getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
步骤也很简单:
- 把key作为参数,执行CRC16算法,获取key对应的slot值。
- 通过该slot值,去slots的map集合中获取jedisPool实例。
- 通过jedisPool实例获取jedis实例,最终完成redis数据存取工作
Jedis实现分布式锁
分布式锁的基本需求:
- 互斥性:只有一个客户端能够持有锁。
- 不会产生死锁:即使持有锁的客户端崩溃,也能保证后续其他客户端可以获取锁。
- 只有持有这把锁的客户端才能解锁。
基于Jedis自己实现一个。
public class DistLock {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
// set支持多个参数 NX(not exist) XX(exist) EX(seconds) PX(million seconds)
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
public static void releaseLock1(Jedis jedis, String lockKey) {
jedis.del(lockKey);
}
public static void releaseLock2(Jedis jedis, String lockKey, String requestId) {
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
// 两个操作不能保证原子性
jedis.del(lockKey);
}
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
参数解读:
- lockkey是Redis key的名称,谁添加成功这个key,就代表谁获取锁成功。比如有一把修改1001账户余额的锁。
- requestld是客户端的ID(设置成value),如果我们要保证只有加锁的客户端才能释放锁,就必须获得客户端的ID(保证第3点,自己才能解锁)
- SET_IF_NOT_EXIST是我们的命令里面加上NX(保证第1点,互斥)。
- SET_WITH_EXPIRE_TIME,PX代表以毫秒为单位设置key的过期时间(保证第2点,不会死锁)。expireTime是自动释放锁的时间,比如5000代表5秒。
释放锁,直接删除key来释放锁可以吗?就像这样:
public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
jedis.del(lockKey);
}
没有对客户端requestld进行判断,可能会释放其他客户端持有的锁。
先判断后删除呢?
public static void wrongReleaseLock2(Jedis jedis,String lockKey,String requestld){
//判断加锁与解锁是不是同一个客户端
if(requestld.equals(jedis.get(lockKey))){
//若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
}
如果在释放锁的时候,这把锁已经不属于这个客户端(例如已经过期,并且被别的客户端获取锁成功了),那就会出现释放了其他客户端的锁的情况。
正解: 所以,要先判断是不是自己加的锁,才能释放。为了保证原子性,我们把判断客户端是否相等和删除key的操作放在Lua脚本里面执行。
public static boolean releaseDistributedLock(Jedis jedis,String lockKey,String requestld){
String script ="if redis.call('get,KEYS[1)-ARGV[1]then return redis.cal('del',KEYS[)else return C end";
Object result = jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestld));
if(RELEASE_SUCCESS.equals(result)){
return true;
}
return false;
}
Pipeline
我们平时说Redis是单线程的,说的是Redis的请求是单线程处理的,只有上一个命令的结果响应以后,下一个命令才会处理。
如果要一次操作10万个key,客户端跟服务端就要交互10万次,排队的时间加上网络通信的时间,就会慢得不得了。举个例子,假设一次交互的网络延迟是1毫秒,客户端1秒钟最多也只能发送1000个命令。这个就太浪费服务端的性能了。
能不能像把一组命令组装在一起发送给Redis服务端执行,然后一次性获得返回结果呢?这个就是Pipeline的作用。Pipeline通过一个队列把所有的命令缓存起来,然后把多个命令在一次连接中发送给服务器。
Jedis Pipeline
public class PipelineGet {
public static void main(String[] args) {
new Thread(){
public void run(){
Jedis jedis = new Jedis("192.168.44.181", 6379);
Set<String> keys = jedis.keys("batch*");
List<String> result = new ArrayList();
long t1 = System.currentTimeMillis();
for (String key : keys) {
result.add(jedis.get(key));
}
for (String src : result) {
//System.out.println(src);
}
System.out.println("直接get耗时:"+(System.currentTimeMillis() - t1));
}
}.start();
new Thread(){
public void run(){
Jedis jedis = new Jedis("192.168.44.181", 6379);
//jedis.auth("qingshan@gupao666");
Set<String> keys = jedis.keys("batch*");
List<Object> result = new ArrayList();
Pipeline pipelined = jedis.pipelined();
long t1 = System.currentTimeMillis();
for (String key : keys) {
pipelined.get(key);
}
result = pipelined.syncAndReturnAll();
for (Object src : result) {
//System.out.println(src);
}
System.out.println("Pipeline get耗时:"+(System.currentTimeMillis() - t1));
}
}.start();
}
}
public class PipelineSet {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.44.181", 6379);
Pipeline pipelined = jedis.pipelined();
long t1 = System.currentTimeMillis();
for (int i=0; i < 1000000; i++) {
pipelined.set("batch"+i,""+i);
}
pipelined.syncAndReturnAll();
long t2 = System.currentTimeMillis();
System.out.println("耗时:"+(t2-t1)+"ms");
}
}
要实现Pipeline,既要服务端的支持,也要客户端的支持。对于服务端来说,需要能够处理客户端通过一个TCP连接发来的多个命令,并且逐个地执行命令一起返回。
对于客户端来说,要把多个命令缓存起来,达到一定的条件就发送出去,最后才处理Redis的应答(这里也要注意对客户端内存的消耗)。
jedis-pipeline的client-buffer限制:8192bytes,客户端堆积的命令超过8M时,会发送给服务端。
源码:redis.clients.util.RedisOutputStream.java 38行
public RedisOutputStream(final OutputStream out) {
this(out, 8192);
}
pipeline对于命令条数没有限制,但是命令可能会受限于TCP包大小。
需要注意的是,并不是所有的业务场景都要用pipeline.
如果某些操作需要马上得到Redis操作是否成功的结果,这种场景就不适合。有些场景,例如批量写入数据,对于结果的实时性和成功性要求不高,就可以用Pipeline.
Lettuce
https://lettuce.io/
与Jedis相比,Lettuce则完全克服了其线程不安全的缺点:Lettuce是一个可伸缩的线程安全的Redis客户端,支持同步、异步和响应式模式(Reactive)。多个线程可以共享一个连接实例,而不必担心多线程并发问题。
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-data-redis</artifactld>
</dependency>
同步调用:
public class LettuceSyncTest {
public static void main(String[] args) {
// 创建客户端
RedisClient client = RedisClient.create("redis://192.168.44.181:6379");
// 线程安全的长连接,连接丢失时会自动重连
StatefulRedisConnection<String, String> connection = client.connect();
// 获取同步执行命令,默认超时时间为 60s
RedisCommands<String, String> sync = connection.sync();
// 发送get请求,获取值
sync.set("shihui:sync","lettuce-sync-666" );
String value = sync.get("shihui:sync");
System.out.println("------"+value);
//关闭连接
connection.close();
//关掉客户端
client.shutdown();
}
}
异步调用:
public class LettuceASyncTest {
public static void main(String[] args) {
RedisClient client = RedisClient.create("redis://192.168.44.181:6379");
// 线程安全的长连接,连接丢失时会自动重连
StatefulRedisConnection<String, String> connection = client.connect();
// 获取异步执行命令api
RedisAsyncCommands<String, String> commands = connection.async();
// 获取RedisFuture<T>
commands.set("shihui:async","lettuce-async-666");
RedisFuture<String> future = commands.get("shihui:async");
try {
String value = future.get(60, TimeUnit.SECONDS);
System.out.println("------"+value);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
e.printStackTrace();
}
}
}
Lettuce基于Netty框架构建,支持Redis的全部高级功能,如发布订阅、事务、lua脚本、Sentinel、集群、Pipeline支持连接池。
Lettuce是Spring Boot 2.x默认的客户端,替换了Jedis,集成之后我们不需要单独使用它,直接调用Spring的RedisTemplate操作,连接和创建和关闭也不需要我们操心。
Redisson
https://redisson.org/
https://github.com/redisson/redisson/wiki/目录
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),提供了分布式和可扩展的Java数据结构,比如分布式的Map,List.,Queue,Set,不需要自己去运行一个服务实现。
基于Netty实现,采用非阻塞10,性能高;支持异步请求。
支持连接池、pipeline、LUA Scripting.Redis Sentinel,Redis Cluster不支持事务,官方建议以LUA Scripting代替事务
主从、哨兵、集群都支持。Spring也可以配置和注入RedissonClient。
在Redisson里面提供了更加简单的分布式锁的实现。
加锁:
public static void main(String[]args)throws InterruptedException{
RLock rLock-redissonClient.getLock("updateAccount");
//最多等待100秒、上锁10s以后自动解锁
if(rLock.tryLock(100,10,TimeUnit.SECONDS)){
System.out.println("获取锁成功");
}
// do something
rLock.unlock();
}
在获得RLock之后,只需要一个tryLock方法,里面有3个参数:
1,watiTime:获取锁的最大等待时间,超过这个时间不再尝试获取锁
2,leaseTime:如果没有调用unlock,超过了这个时间会自动释放锁
3,TimeUnit:释放时间的单位
Redisson的分布式锁是怎么实现的呢?
在加锁的时候,在Redis写入了一个HASH,key是锁名称,field是线程名称,value是1(表示锁的重入次数)。
源码:
tryLock()——tryAcquire()——tryAcquireAsync()——tryLockInnerAsync()
最终也是调用了一段Lua脚本。里面有一个参数,两个参数的值。
//KEYS[1] 锁名称 updateAccount
//ARGV[1] key 过期时间 10000ms
//ARGV[2] 线程名称
//锁名称不存在
if(redis.call('exists',KEYS[1])==0)then
//创建一个hash,key=锁名称,field=线程名,value=1
redis.call('hset',KEYS[1],ARGV[2],1);
//设置hash的过期时间
redis.call('pexpire',KEYS[1],ARGV[1);
return nil;
end;
//锁名称存在,判断是否当前线程持有的锁
if(redis.call('hexists',KEYS[1],ARGV[2)==1)then
//如果是,value+1,代表重入次数+1
redis.call('hincrby',KEYS[1],ARGV[2],1);
//重新获得锁,需要重新设置Key的过期时间
redis.call('pexpire',KEYS[1],ARGV[1);
return nil;
end;
//锁存在,但是不是当前线程持有,返回过期时间(毫秒)
return redis.call('pttl',KEYS[1]);
释放锁,源码:
unlock——unlockInnerAsync
//KEYS[1] 锁的名称 updateAccount
//KEYS[2] 频道名称 redisson_lock_channel:{updateAccount}
//ARGV[1] 释放锁的消息 0
//ARGV[2] 锁释放时间 10000
//ARGV[3] 线程名称
//锁不存在(过期或者已经释放了)
if(redis.call('exists',KEYS[1])==0)then
//发布锁已经释放的消息
redis.call('publish',KEYS[2],ARGV[1]);
return 1;
end,
//锁存在,但是,不是当前线程加的锁
if(redis.call('hexists',KEYS[1],ARGV[3])==0)then
return nil;
end;
//锁存在,是当前线程加的锁
//重入次数-1
local counter = redis.call('hincrby',KEYS[1],ARGV[3],-1);
//-1后大于0,说明这个线程持有这把锁还有其他的任务需要执行
if(counter > 0)then
//重新设置锁的过期时间
redis.call('pexpire',KEYS[1],ARGV[2]);
return 0;
else
//-1之后等于0,现在可以删除锁了
redis.call('del',KEYS[1);
//删除之后发布释放锁的消息
reds.call('publish',KEYS[2],ARGV[1]);
return 1;
end;
//其他情况返回nil
return nil;
这个是Redisson里面分布式锁的实现,我们在调用的时候非常简单。
- 业务没执行完,锁到期了怎么办?
答:watchdog(Redisson github wiki) - 集群模式下,如果对多个master加锁,导致重复加锁怎么办?
答:Redisson会自动选择同一个master - 业务没执行完,Redis master挂了怎么办?
没关系,Redis slave还有这个数据。