Spring之RedisTemplate配置与应用转载

原创
小哥 2年前 (2022-12-26) 阅读数 44 #大杂烩

文章链接: liuyueyi.github.io/hexblog/201…

Spring之RedisTemplate配置和使用

Spring针对Redis使用封装更强大Template为了便于使用;先前的Spring它也用于生态系统。redis但直接使用Jedis要执行相应的交互操作,现在只需查看它RedisTemplate它是如何实现的,使用起来是否更方便?

I. 基本配置

1. 依赖

仍然采用。Jedis连接池管理,因此除了引入 spring-data-redis 此外,还有jedis依赖,pom添加


    org.springframework.data
    spring-data-redis
    1.8.4.RELEASE



    redis.clients
    jedis
    2.9.0

复制代码

如果需要指定与序列化相关的参数,还可以引入jackson,本文是一个简单的入门级,而不是添加

2. 配置文件

准备redis相关配置参数,常见的有host, port, password, timeout...,以下是一个简单的配置并给出相应的含义

redis.hostName=127.0.0.1
redis.port=6379
redis.password=https://blog.hhui.top
# 连接超时
redis.timeout=10000

#最大自由数
redis.maxIdle=300
#控制一个pool可以分配多少jedis实例,用于替换上述redis.maxActive,如果是jedis 2.4稍后使用此属性
redis.maxTotal=1000
#最大连接设置等待时间。如果超过此时间,将收到异常。设置-1表示没有限制。
redis.maxWaitMillis=1000
#连接的最短空闲时间 默认1800000毫秒(30分钟)
redis.minEvictableIdleTimeMillis=300000
#一次释放的最大连接数,默认3
redis.numTestsPerEvictionRun=1024
#逐出扫描的时间间隔(毫秒) 如果为阴性,驱逐线程未运行, 默认-1
redis.timeBetweenEvictionRunsMillis=30000
#从池中删除连接之前进行验证,如果测试失败,然后从池中删除连接,并尝试获取另一个
redis.testOnBorrow=true
#空闲时检查有效性, 默认false
redis.testWhileIdle=true
复制代码

说明

  • redis请记住设置密码,尤其是在允许远程访问时。如果没有密码和默认端口号,很容易扫描和注入脚本,然后开始为人们挖掘(个人经验...)

II. 使用和测试

按照一般思路,首先要加载上述配置并创建redis连接池,然后实例化RedisTemplate对象,并最终保持此强度以开始各种读写操作

1. 配置类

使用JavaConfig配置方式,主要有两种Bean,读取配置文件以设置各种参数。 RedisConnectionFactory 和预期的 RedisTemplate

@Configuration
@PropertySource("classpath:redis.properties")
public class RedisConfig extends JCacheConfigurerSupport {
    @Autowired
    private Environment environment;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory fac = new JedisConnectionFactory();
        fac.setHostName(environment.getProperty("redis.hostName"));
        fac.setPort(Integer.parseInt(environment.getProperty("redis.port")));
        fac.setPassword(environment.getProperty("redis.password"));
        fac.setTimeout(Integer.parseInt(environment.getProperty("redis.timeout")));
        fac.getPoolConfig().setMaxIdle(Integer.parseInt(environment.getProperty("redis.maxIdle")));
        fac.getPoolConfig().setMaxTotal(Integer.parseInt(environment.getProperty("redis.maxTotal")));
        fac.getPoolConfig().setMaxWaitMillis(Integer.parseInt(environment.getProperty("redis.maxWaitMillis")));
        fac.getPoolConfig().setMinEvictableIdleTimeMillis(
                Integer.parseInt(environment.getProperty("redis.minEvictableIdleTimeMillis")));
        fac.getPoolConfig()
                .setNumTestsPerEvictionRun(Integer.parseInt(environment.getProperty("redis.numTestsPerEvictionRun")));
        fac.getPoolConfig().setTimeBetweenEvictionRunsMillis(
                Integer.parseInt(environment.getProperty("redis.timeBetweenEvictionRunsMillis")));
        fac.getPoolConfig().setTestOnBorrow(Boolean.parseBoolean(environment.getProperty("redis.testOnBorrow")));
        fac.getPoolConfig().setTestWhileIdle(Boolean.parseBoolean(environment.getProperty("redis.testWhileIdle")));
        return fac;
    }

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redis = new RedisTemplate<>();
        redis.setConnectionFactory(redisConnectionFactory);
        redis.afterPropertiesSet();
        return redis;
    }
}
复制代码

2. 测试和使用

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RedisConfig.class})
public class RedisTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testRedisObj() {
        Map properties = new HashMap<>();
        properties.put("123", "hello");
        properties.put("abc", 456);

        redisTemplate.opsForHash().putAll("hash", properties);

        Map ans = redisTemplate.opsForHash().entries("hash");
        System.out.println("ans: " + ans);
    }
}
复制代码

执行后的输出如下

ans: {123=hello, abc=456}
复制代码

从上面的配置和实现来看,非常简单,基本上没有圆圈,而是使用redis-cli均匀,但找不到 hash 这个key的内容

127.0.0.1:6379> get hash
(nil)
127.0.0.1:6379> keys *
1) "xacxedx00x05tx00x04hash"
复制代码

使用代码检查没有问题,直接控制台连接,发现此key这不是我们所期望的,加上几个前缀,why ?

3. 序列化问题

为了解决上述问题debug进去看看是什么引起的。

对应的源代码位置:

// org.springframework.data.redis.core.AbstractOperations#rawKey

byte[] rawKey(Object key) {
    Assert.notNull(key, "non null key required");
    return this.keySerializer() == null && key instanceof byte[] ? (byte[])((byte[])key) : this.keySerializer().serialize(key);
}
复制代码

你可以看到这个key不是我们期望的 key.getBytes() , 相反,它调用 this.keySerializer().serialize(key) ,而debug默认的结果Serializer是 JdkSerializationRedisSerializer

然后逐步深入。链接如下

// org.springframework.core.serializer.support.SerializingConverter#convert

// org.springframework.core.serializer.DefaultSerializer#serialize

public class DefaultSerializer implements Serializer {
    public DefaultSerializer() {
    }

    public void serialize(Object object, OutputStream outputStream) throws IOException {
        if (!(object instanceof Serializable)) {
            throw new IllegalArgumentException(this.getClass().getSimpleName() + " requires a Serializable payload but received an object of type [" + object.getClass().getName() + "]");
        } else {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
        }
    }
}
复制代码

因此,具体实施非常明确,即 ObjectOutputStream ,这件事是Java最原始的序列化反序列化流工具将包含类型信息,因此它将携带字符串前缀。

因此,要解决这个问题,替换原来的 JdkSerializationRedisSerializer ,改为String在某种程度上 StringRedisSerializer ,所以在RedisTemplate的配置,略有修改

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate redis = new RedisTemplate<>();
    redis.setConnectionFactory(redisConnectionFactory);

    // 设置redis的String/Value默认序列化方法
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    redis.setKeySerializer(stringRedisSerializer);
    redis.setValueSerializer(stringRedisSerializer);
    redis.setHashKeySerializer(stringRedisSerializer);
    redis.setHashValueSerializer(stringRedisSerializer);

    redis.afterPropertiesSet();
    return redis;
}
复制代码

再次执行,结果是尴尬的事情出现,抛出异常,类型转换失败

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

    at org.springframework.data.redis.serializer.StringRedisSerializer.serialize(StringRedisSerializer.java:33)
    at org.springframework.data.redis.core.AbstractOperations.rawHashValue(AbstractOperations.java:171)
    at org.springframework.data.redis.core.DefaultHashOperations.putAll(DefaultHashOperations.java:129)
    ...
复制代码

查看先前的测试案例,map中的value有integer,而 StringRedisSerializer 收到的参数必须为String,所以不要使用这个,根据外观编写另一个兼容的

public class DefaultStrSerializer implements RedisSerializer {
    private final Charset charset;

    public DefaultStrSerializer() {
        this(Charset.forName("UTF8"));
    }

    public DefaultStrSerializer(Charset charset) {
        Assert.notNull(charset, "Charset must not be null!");
        this.charset = charset;
    }

    @Override
    public byte[] serialize(Object o) throws SerializationException {
        return o == null ? null : String.valueOf(o).getBytes(charset);
    }

    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        return bytes == null ? null : new String(bytes, charset);

    }
}
复制代码

然后你就可以开始快乐地玩耍了。,执行后测试

keys *
1) "xacxedx00x05tx00x04hash"
2) "hash"
127.0.0.1:6379> hgetAll hash
1) "123"
2) "hello"
3) "abc"
4) "456"
复制代码

III. RedisTemplate使用姿势

1. opsForXXX

短暂地来这里。RedisTemplate不同数据结构的姿势使用(String, List, ZSet, Hash)Read封装了比较使用的调用方法。 opsForXXX

// hash 数据结构操作
org.springframework.data.redis.core.RedisTemplate#opsForHash

// list
org.springframework.data.redis.core.RedisTemplate#opsForList

// string
org.springframework.data.redis.core.RedisTemplate#opsForValue

// set
org.springframework.data.redis.core.RedisTemplate#opsForSet

// zset
org.springframework.data.redis.core.RedisTemplate#opsForZSet
复制代码

2. execute

除了上述这种方式的使用之外,另一种常见的方式是直接使用execute一个简单的。case如下

@Test
public void testRedis() {
    String key = "hello";
    String value = "world";
    redisTemplate.execute((RedisCallback) con -> {
        con.set(key.getBytes(), value.getBytes());
        return null;
    });

    String asn = redisTemplate.execute((RedisCallback) con -> new String(con.get(key.getBytes())));
    System.out.println(asn);

    String hkey = "hKey";
    redisTemplate.execute((RedisCallback) con -> {
        con.hSet(hkey.getBytes(), "23".getBytes(), "what".getBytes());
        return null;
    });

    Map map = redisTemplate.execute((RedisCallback>) con -> con.hGetAll(hkey.getBytes()));
    for (Map.Entry entry : map.entrySet()) {
        System.out.println("key: " + new String(entry.getKey()) + " | value: " + new String(entry.getValue()));
    }
}
复制代码

输出结果如下

world
key: 23 | value: what
复制代码

3. 区别

一个自然的问题是,上述两种方法之间的区别是什么?

opsForXXX 的底层称为。execute实现这一点的主要方法是封装一些使用姿势并定义序列化,这更简单,使用更方便。通过这种方式,引入的小号每次都会创建一个新的小号。 DefaultXXXOperations 对象,在此基础上再转一步会带来额外的性能和内存开销吗?没有测试,但在个人感觉小的情况下,应该没有明显的影响。和qps在非常高的情况下,这种方便的优化可以带来的帮助估计不会太大。

作者:A Grey
链接:https://juejin.im/post/5b1e5d4ee51d4506c60e4275
来源:掘金
版权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

版权声明

所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除

热门