redis基础篇——Redis客户端(Jedis,Lettuce,Redisson)

本文介绍了Redis的三种Java客户端——Jedis, Lettuce和Redisson。Jedis虽功能全面但存在线程安全问题,可通过连接池解决。Lettuce是线程安全的,支持同步、异步和响应式模式,是Spring Boot 2.x的默认客户端。Redisson提供分布式数据结构,支持多种模式并内置分布式锁。" 132328422,19694743,Apache Flink配置详解:优化flink-conf.yaml,"['大数据', 'flink']

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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客户端。

  1. 建立Socket连接
  2. OutputStream写入数据(发送命令到服务端)
  3. 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连接池的关系?


  1. 程序启动初始化集群环境,读取配置文件中的节点配置,无论是主从,无论多少个,只拿第一个,获取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();
    			}
    		}
    	}
    }
    
  2. discoverClusterNodesAndSlots方法,用获取的redis连接实例执行clusterSlots()方法,实际执行redis服务端cluster slots命令,获取虚拟槽信息。该集合的基本信息为[long,long,List,List],第一,二个元素是该节点负责槽点的起始位置,第三个元素是主节点信息,第四个元素为主节点对应的从节点信息。该list的基本信息为[string,int,string],.第一个为host信息,第二个为port信息,第三个为唯id。
    在这里插入图片描述

  3. 获取有关节点的槽点信息后,调用getAssignedSlotArray(slotinfo)来获取所有的槽点值。

  4. 再获取主节点的地址信息,调用generateHostAndPort(hostInfo)方法,生成一个hostAndPort对象。

  5. 在assignSlotsToNode方法中,再根据节点地址信息来设置节点对应的JedisPool,即设置Map<String,JedisPool> nodes的值。

  6. 接下来判断若此时节点信息为主节点信息时,则调用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位是主从端口的信息
    			forint 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));

步骤也很简单:

  1. 把key作为参数,执行CRC16算法,获取key对应的slot值。
  2. 通过该slot值,去slots的map集合中获取jedisPool实例。
  3. 通过jedisPool实例获取jedis实例,最终完成redis数据存取工作

Jedis实现分布式锁


分布式锁的基本需求:

  1. 互斥性:只有一个客户端能够持有锁。
  2. 不会产生死锁:即使持有锁的客户端崩溃,也能保证后续其他客户端可以获取锁。
  3. 只有持有这把锁的客户端才能解锁。

基于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;
    }
}

参数解读:

  1. lockkey是Redis key的名称,谁添加成功这个key,就代表谁获取锁成功。比如有一把修改1001账户余额的锁。
  2. requestld是客户端的ID(设置成value),如果我们要保证只有加锁的客户端才能释放锁,就必须获得客户端的ID(保证第3点,自己才能解锁)
  3. SET_IF_NOT_EXIST是我们的命令里面加上NX(保证第1点,互斥)。
  4. 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(10010,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里面分布式锁的实现,我们在调用的时候非常简单。


  1. 业务没执行完,锁到期了怎么办?
    答:watchdog(Redisson github wiki)
  2. 集群模式下,如果对多个master加锁,导致重复加锁怎么办?
    答:Redisson会自动选择同一个master
  3. 业务没执行完,Redis master挂了怎么办?
    没关系,Redis slave还有这个数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值