RedisTemplate源碼分析以及Redis常見問題


1. RedisTemplate 默認配置下底層實現

使用jedis(spring-boot 1.x)或者lettuce(spring-boot 2.x)操作redis的

spring-boot 1.5.7

spring-data-redis 1.8.7

配置文件

# redis
spring.redis.host=172.168.32.145
spring.redis.password=
spring.redis.port=6379
spring.redis.database=7

單元測試代碼,獲取1000次 字符類型的redis數值,以下代碼為並行執行代碼

@Test
public void getStringValue() throws Exception {
    final String key = "cloud_search_using_record";
    for (int i = 0; i < 1000; i++) {
        new Thread(()->{
            stringRedisTemplate.opsForValue().get(key);
        }).start();
    }
    Thread.sleep(1000000);
}

DefaultValueOperations.class , 25行

public V get(Object key) {
	return this.execute(new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key) {
		protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
			return connection.get(rawKey);
		}
	}, true);
}

AbstractOperations.class , 56行

<T> T execute(RedisCallback<T> callback, boolean b) {
	return this.template.execute(callback, b);
}

RedisTemplate.class,126行

conn = RedisConnectionUtils.getConnection(factory);

RedisConnectionUtils.class, 61行

RedisConnection conn = factory.getConnection();

debug調試 JedisConnectionFactory.class 247行,進行斷點查看jedis對象內存地址,當串行執行時,一般的會出現每次都是一樣的,並行執行時jedis 地址是不斷在一定范圍變化的。

public RedisConnection getConnection() {
    if (this.cluster != null) {
        return this.getClusterConnection();
    } else {
        Jedis jedis = this.fetchJedisConnector();
        JedisConnection connection = this.usePool ? new JedisConnection(jedis, this.pool, this.dbIndex, this.clientName) : new JedisConnection(jedis, (Pool)null, this.dbIndex, this.clientName);
        connection.setConvertPipelineAndTxResults(this.convertPipelineAndTxResults);
        return this.postProcessConnection(connection);
    }
}    
protected Jedis fetchJedisConnector() {
	try {
		if (this.usePool && this.pool != null) {
			return (Jedis)this.pool.getResource();
		} else {
			Jedis jedis = new Jedis(this.getShardInfo());
			jedis.connect();
			this.potentiallySetClientName(jedis);
			return jedis;
		}
	} catch (Exception var2) {
		throw new RedisConnectionFailureException("Cannot get Jedis connection", var2);
	}
}

獲取到的jedis是封裝好的RedisConnection對象,在RedisTemplate.class類execute方法中的action.doInRedis(connToExpose); 進行操作Redis並返回結果。

Spring-boot 1.x版本,默認使用Jedis 連接池,也可以主動不使用連接池,設置JedisConnectionFactory工廠類屬性是否使用連接池為false,jedisConnectionFactory.setUsePool(false);

spring-boot 2.3.8.RELEASE

spring-data-redis 2.3.6.RELEASE

2.x版本默認優先使用lettuce,如果非要使用jedis 則必須去掉lettuce依賴和RedisAutoConfiguration

配置文件

# redis
spring.redis.host=172.168.32.145
spring.redis.password=
spring.redis.port=6379
spring.redis.database=7

debug調試 RedisTemplate.class,145行。進行斷點查看((LettuceConnectionFactory.SharedConnection)((LettuceConnectionFactory)factory).connection).connection對象內存地址,無論串行還行並行,地址都一樣。

conn = RedisConnectionUtils.getConnection(factory);

這是因為 shareNativeConnection默認值是true,即 默認情況下都使用的同一個本地連接。

當設置本地共享連接為false時,在此處可以看到連接每次都不一樣。

另外LettuceConnectionLettuceConnectionFactory創建時,並沒有立即創建連接,而是只創建了LettuceConnection 對象,在使用該對象操作Redis時會檢查是否已經創建共享連接,沒有的話進行創建。

詳細過程如下:

RedisTemplate.class,153行

public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
        ...
        // 如果是redisTemplate 則直接返回連接本身,如果是stringRedisTemplate則返回DefaultStringRedisConnection,連接RedisConnection對象引用給屬性delegate
		RedisConnection connToUse = this.preProcessConnection(conn, existingConnection);
		...
        // 進行callback執行,入參為RedisConnection/DefaultStringRedisConnection     
		T result = action.doInRedis(connToExpose);
		...
}

DefaultStringRedisConnection.class 242行

public byte[] get(byte[] key) {
	return (byte[])this.convertAndReturn(this.delegate.get(key), this.identityConverter);
}

DefaultedRedisConnection.class 224行

default byte[] get(byte[] key) {
	return this.stringCommands().get(key);
}

LettuceStringCommands 31行

public byte[] get(byte[] key) {
	Assert.notNull(key, "Key must not be null!");

	try {
		...
            // 執行獲取連接
			return (byte[])this.getConnection().get(key);
		}
	} catch (Exception var3) {
		throw this.convertLettuceAccessException(var3);
	}
}

LettuceConnection.class 664行代碼 getConnection()

protected RedisClusterCommands<byte[], byte[]> getConnection() {
	if (this.isQueueing()) {
		return this.getDedicatedConnection();
	} else {
		if (this.asyncSharedConn != null) {
			if (this.asyncSharedConn instanceof StatefulRedisConnection) {
				return ((StatefulRedisConnection)this.asyncSharedConn).sync();
			}

			if (this.asyncSharedConn instanceof StatefulRedisClusterConnection) {
				return ((StatefulRedisClusterConnection)this.asyncSharedConn).sync();
			}
		}
		// 獲取專用(非共享)連接
		return this.getDedicatedConnection();
	}
}

LettuceConnection.class 686行代碼 getDedicatedConnection()

RedisClusterCommands<byte[], byte[]> getDedicatedConnection() {
	StatefulConnection<byte[], byte[]> connection = this.getOrCreateDedicatedConnection();
	if (connection instanceof StatefulRedisConnection) {
		return ((StatefulRedisConnection)connection).sync();
	} else if (connection instanceof StatefulRedisClusterConnection) {
		return ((StatefulRedisClusterConnection)connection).sync();
	} else {
		throw new IllegalStateException(String.format("%s is not a supported connection type.", connection.getClass().getName()));
	}
}

LettuceConnection.class 708行代碼 getOrCreateDedicatedConnection()

private StatefulConnection<byte[], byte[]> getOrCreateDedicatedConnection() {
	if (this.asyncDedicatedConn == null) {
        // 獲取連接
		this.asyncDedicatedConn = this.doGetAsyncDedicatedConnection();
	}

	return this.asyncDedicatedConn;
}

獲取連接核心代碼

//LettuceConnection.doGetAsyncDedicatedConnection()
StatefulConnection connection = this.connectionProvider.getConnection(StatefulConnection.class);
//LettucePoolingConnectionProvider.getConnection()
GenericObjectPool pool = (GenericObjectPool)this.pools.computeIfAbsent(connectionType, (poolType) -> {
	return ConnectionPoolSupport.createGenericObjectPool(() -> {
		return this.connectionProvider.getConnection(connectionType);
	}, this.poolConfig, false);
});
...
StatefulConnection<?, ?> connection = (StatefulConnection)pool.borrowObject();
...

2. 關於連接關閉

RedisTemplate.class,操作完redis 后finally里調用releaseConnection

public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
    Assert.isTrue(this.initialized, "template not initialized; call afterPropertiesSet() before using it");
    Assert.notNull(action, "Callback object must not be null");
    RedisConnectionFactory factory = this.getConnectionFactory();
    RedisConnection conn = null;
...
} finally {
    // 執行完成之后釋放連接:事務檢查以及connection.close();
    RedisConnectionUtils.releaseConnection(conn, factory);
}

3. 關於jedis有一點官網描述需要注意

using Jedis in a multithreaded environment

You shouldn't use the same instance from different threads because you'll have strange errors. And sometimes creating lots of Jedis instances is not good enough because it means lots of sockets and connections, which leads to strange errors as well. A single Jedis instance is not threadsafe! To avoid these problems, you should use JedisPool, which is a threadsafe pool of network connections. You can use the pool to reliably create several Jedis instances, given you return the Jedis instance to the pool when done. This way you can overcome those strange errors and achieve great performance.

To use it, init a pool:

JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");

Jedis繼承BinaryJedis,而BinaryJedis成員變量Client,Client-->BinaryClient-->Connection

Connection的connect方法中可以看到讀寫流是成員變量也並沒有做並發限制,所以現成不安全的。

this.outputStream = new RedisOutputStream(this.socket.getOutputStream());
this.inputStream = new RedisInputStream(this.socket.getInputStream());

所以一般的,使用Jedis 時 必須時線程池(連接池)形式,每個線程獨立使用一個Jedis 連接對象。

4. spring-boot 2.x 使用Lettuce優先Jedis官網解釋

Spring 優先使用Lettuce,一個是因為Jedis線程不安全,另一個連接池是以每個實例的物理連接為代價的,這會增加Redis連接的數量。而Lettuce 基於Netty實現,支持多線程共享連接,不用擔心並發線程數影響到Redis連接數。

5. RedisTemplate 使用Pipeline示例

final String key = "cloud_search_using_record";
RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
List<String> value = redisTemplate.executePipelined(new RedisCallback<String>() {
	@Override
	public String doInRedis(RedisConnection connection) throws DataAccessException {
		// get
		connection.get(key.getBytes());
		// set
//                connection.set(serializer.serialize(key), serializer.serialize(value), Expiration.seconds(seconds), RedisStringCommands.SetOption.UPSERT);
		return null;
	}
}, serializer);
Assert.assertNotNull(value);
Assert.assertEquals(1, value.size());
System.out.println(value.get(0));

6. 常見錯誤原因以及排查

1.Connection reset 錯誤

服務端終止異常連接導致

原因是Tcp握手過程中,服務器返回了RST標志位,但是客戶端還在進行輸入輸出數據,拋出此錯誤

RST表示復位,用來異常的關閉連接,在TCP的設計中它是不可或缺的

2.SocketTimeoutException: connect timed out錯誤

網絡波動原因

服務器連接達到最大限制,應當檢查代碼中是否有連接未關閉

3.Could not get a resource from the pool 錯誤,連接池無空閑連接

Timeout waiting for idle object 等待獲取連接超時

  1. 連接池大小調整
  2. 從連接池獲取連連接等待超時調整
  3. 檢查連接有無歸還連接池或者關閉
4.當配置獲取連接等待時間為-1,發生操作redis阻塞情況

檢查是否關閉連接或者歸還連接,這是形成無限等待了

5.Unexpected end of stream錯誤

服務端超時配置過短,導致客戶端在用連接失效


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM