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時,在此處可以看到連接每次都不一樣。
另外LettuceConnection
從LettuceConnectionFactory
創建時,並沒有立即創建連接,而是只創建了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 等待獲取連接超時
- 連接池大小調整
- 從連接池獲取連連接等待超時調整
- 檢查連接有無歸還連接池或者關閉
4.當配置獲取連接等待時間為-1,發生操作redis阻塞情況
檢查是否關閉連接或者歸還連接,這是形成無限等待了
5.Unexpected end of stream錯誤
服務端超時配置過短,導致客戶端在用連接失效