Redis进阶学习01---基础回顾

2022-05-09 14:03:16 浏览数 (1)

Redis进阶学习01---基础回顾

  • redis使用docker安装
  • 常用命令
    • 常用通用命令
    • String类型常用命令
    • key的层级表示
    • Hash类型常用命令
    • List类型
    • set类型
    • sortedSet类型
  • Redis客户端
    • Jedis
      • Jedis连接池
    • SpringDataRedis
      • 快速入门
      • 序列化问题 源码追踪分析
      • 替换序列化器解决乱码问题
      • 测试
      • 额外内存占用如何解决
      • RedisTemplate的两种序列化实践方案
      • hashOperations使用说明

本系列针对redis进阶学习,因此对于基础部分不会详细讲述

redis使用docker安装

  • 准备存放redis配置文件数据目录
代码语言:javascript复制
# 这里我们在 /home/docker 下创建
mkdir /home/docker/redis/{conf,data} -p
cd /home/docker/redis

注意:后面所有的操作命令都要在这个目录/home/docker/redis下进行

  • 下载一份默认的redis配置文件
代码语言:javascript复制
# 获取 redis 的默认配置模版
# 这里主要是想设置下 redis 的 log / password / appendonly
# redis 的 docker 运行参数提供了 --appendonly yes 但没 password
wget https://gitee.com/hanxt/boot-launch/raw/master/src/main/resources/otherconfig/redis.conf -O conf/redis.conf

# 直接替换编辑
sed -i 's/logfile ""/logfile "access.log"/' conf/redis.conf;
sed -i 's/# requirepass foobared/requirepass 123456/' conf/redis.conf;
sed -i 's/appendonly no/appendonly yes/' conf/redis.conf;
sed -i 's/bind 127.0.0.1/bind 0.0.0.0/' conf/redis.conf;
  • sed -i是linux文件替换命令,替换格式为s/被替换的内容/替换之后的内容/
  • 替换logfile ""为logfile “access.log”,指定日志文件名称为access.log---->指定日志文件的名称
  • 替换# requirepass foobared为requirepass 123456,指定访问密码为123456—>配置登录密码,auth 123456
  • 替换“appendonly no“为”appendonly yes”,开启appendonly模式–》持久化配置
  • 替换绑定IP“bind 127.0.0.1”为“bind 0.0.0.0”—>任意ip可以访问
  • 创建容器
代码语言:javascript复制
# 创建并运行一个名为 myredis 的容器
docker run 
-p 6379:6379 
-v $PWD/data:/data 
-v $PWD/conf/redis.conf:/etc/redis/redis.conf 
--privileged=true 
--name myredis 
-d redis:5.0.5 redis-server /etc/redis/redis.conf
  • 查询当前容器信息
代码语言:javascript复制
# 查看活跃的容器
docker ps
# 如果没有 myredis 说明启动失败 查看错误日志
docker logs myredis
# 查看 myredis 的 ip 挂载 端口映射等信息
docker inspect myredis
# 查看 myredis 的端口映射
docker port myredis
  • 访问当前容器
代码语言:javascript复制
docker exec -it myredis bash
redis-cli

常用命令

常用通用命令

  • KEYS: 查看符合模板的所有key,模糊查询效率较低,不建议生产环境使用

keys命令用法

  • DEL: 删除一个指定的key

del命令用法

  • EXISTS:判断key是否存在

exists命令用法

  • EXPIRE:给一个key设置有效期,有效期到期时刻该key会被自动删除

命令的官方链接介绍从此处开始,就不贴了,感兴趣的自己打开官网查看

  • TTL:查看一个key的剩余有效期

String类型常用命令

key的层级表示

redis没有mysql中的table概念,但是如果所有key的一股脑扔到redis的同一个数据库里面,那也太乱了,因此我们可以通过下面这种Key的层级表示方法,来管理不同的key,很好的将不同的key进行了区分

Redis的可以允许有多个单词形成层级结构,多个单词之间用":"隔开,格式如下:

代码语言:javascript复制
项目名:业务名:类型:id

例如:项目名叫dhy,有user和peo两种不同类型的数据,我们可以这样定义key

  • user相关的key: dhy:user:1
  • product相关的key: dhy:product:1

Hash类型常用命令


List类型

set类型

sortedSet类型


Redis客户端

Jedis

  • 引入依赖
代码语言:javascript复制
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.1.1</version>
        </dependency>
  • 建立连接
代码语言:javascript复制
    /**
     * 1.建立连接
     * 2.设置密码
     * 3.选择库
     */
    @BeforeEach
    public void connect() {
       jedis=new Jedis("redis服务器ip地址",6379);
       jedis.auth("password");
       jedis.select(0);
   }
  • 测试
代码语言:javascript复制
   @Test
   public void testString(){
       String res = jedis.set("name", "dhy");
       System.out.println("res= " res);
       String name = jedis.get("name");
       System.out.println("name= " name);
   }
  • 释放资源
代码语言:javascript复制
   @AfterEach
  public void close()
  {
      if(jedis!=null)
      {
          jedis.close();
      }
  }

Jedis连接池

Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用Jedis连接池代替Jedsi直接连接的方式

代码语言:javascript复制
public class JedsiConnectionFactory {
    private static final JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        //最大连接
        jedisPoolConfig.setMaxTotal(8);
        //最大空闲连接
        jedisPoolConfig.setMaxIdle(8);
        //最小空闲连接
        jedisPoolConfig.setMinIdle(0);
        //设置最长等待时间 ms
        jedisPoolConfig.setMaxWaitMillis(200);
        jedisPool=new JedisPool(jedisPoolConfig,"redis服务器ip地址",6380,1000,"password");
    }

    /**
     * 返回一个jedis对象
     */
    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}
  • 使用
代码语言:javascript复制
public class JedisTest {
    private Jedis jedis;
    /**
     * 1.建立连接
     * 2.设置密码
     * 3.选择库
     */
    @BeforeEach
    public void connect() {
       jedis=JedsiConnectionFactory.getJedis();
       jedis.auth("126433zdh");
       jedis.select(0);
   }

   @Test
   public void testString(){
       String res = jedis.set("name", "dhy");
       System.out.println("res= " res);
       String name = jedis.get("name");
       System.out.println("name= " name);
   }

   @AfterEach
  public void close()
  {
      if(jedis!=null)
      {
          jedis.close();
      }
  }
}

为什么都用了jedis连接池,还要调用jedis的close方法呢?

代码语言:javascript复制
    public void close() {
    //如果当前jedis与一个连接池关联,那么他会被放入连接池中
    //否则销毁当前连接
        if (this.dataSource != null) {
            Pool<Jedis> pool = this.dataSource;
            this.dataSource = null;
            if (this.isBroken()) {
                pool.returnBrokenResource(this);
            } else {
                pool.returnResource(this);
            }
        } else {
            this.connection.close();
        }

    }

SpringDataRedis

快速入门

  • 引入依赖
代码语言:javascript复制
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
  • 配置文件
代码语言:javascript复制
spring:
  redis:
    host: 110.40.155.17
    port: 6380
    password: 126433zdh
    lettuce:
    #只有自动配置连接池的依赖,连接池才会生效
      pool:
        max-active: 8 #最大连接
        max-idle: 8 #最大空闲连接
        min-idle: 0 #最小空闲连接
        max-wait: 100 #连接等待时间

springboot默认使用lettuce作为jedis客户端,因为在引入spring-boot-satrt-redis依赖的时候,会自动引入lettuce的依赖

  • 注入restTemplate
代码语言:javascript复制
    @Autowired
    RedisTemplate redisTemplate;
  • 测试
代码语言:javascript复制
    @Test
    void testString(){
         redisTemplate.opsForValue().set("age",18);
        Object age = redisTemplate.opsForValue().get("age");
        System.out.println(age);
    }

序列化问题 源码追踪分析

为什么通过RedisTemplate存入redis的key存在乱码?

因为RedisTemplate会分别使用key和value的序列化器,对key和value进行序列化后存入redis,又因为默认使用的序列化器时jdk的objectoutputStream所以才会有乱码存在

源码追踪:

RedisTemplate内部有四个序列化器:

代码语言:javascript复制
	private @Nullable RedisSerializer keySerializer = null;
	 private @Nullable RedisSerializer valueSerializer = null;
	 private @Nullable RedisSerializer hashKeySerializer = null;
	 private @Nullable RedisSerializer hashValueSerializer = null;

RedisTemplate实现了initlizeBean接口,因此重写了afterPropertiesSet方法,会在RedisTemplate创建后的初始化阶段被调用:

代码语言:javascript复制
	@Override
	public void afterPropertiesSet() {
       //父类只要是配置好RedisConnectionFactory
		super.afterPropertiesSet();

		boolean defaultUsed = false;

		if (defaultSerializer == null) {
           //默认的序列化器是JdkSerializationRedisSerializer
			defaultSerializer = new JdkSerializationRedisSerializer(
					classLoader != null ? classLoader : this.getClass().getClassLoader());
		}
         //如果我们没有手动去指定这四个序列花旗,默认都会使用jdk的序列化器  
		if (enableDefaultSerializer) {
             
			if (keySerializer == null) {
				keySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (valueSerializer == null) {
				valueSerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashKeySerializer == null) {
				hashKeySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashValueSerializer == null) {
				hashValueSerializer = defaultSerializer;
				defaultUsed = true;
			}
		}

		if (enableDefaultSerializer && defaultUsed) {
			Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
		}

		if (scriptExecutor == null) {
			this.scriptExecutor = new DefaultScriptExecutor<>(this);
		}

		initialized = true;
	}

下面再来看一下具体的set方法:

代码语言:javascript复制
	@Override
	public void set(K key, V value) {
         //rawValue就是具体把传入的值序列化为字节数组的过程
		byte[] rawValue = rawValue(value);
		execute(new ValueDeserializingRedisCallback(key) {

			@Override
			protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
				connection.set(rawKey, rawValue);
				return null;
			}
		}, true);
	}

下面来看一下rawValue:

代码语言:javascript复制
	byte[] rawValue(Object value) {

		if (valueSerializer() == null && value instanceof byte[]) {
			return (byte[]) value;
		}
        //获取到值序列化器,也就是valueSerializer ,然后进行序列化
		return valueSerializer().serialize(value);
	}
代码语言:javascript复制
	public byte[] serialize(@Nullable Object object) {
		if (object == null) {
			return SerializationUtils.EMPTY_ARRAY;
		}
		try {
		//通过内部的类型转换器,完成值类型到字节数组的转化,也就是序列化的过程
			return serializer.convert(object);
		} catch (Exception ex) {
			throw new SerializationException("Cannot serialize", ex);
		}
	}

JdkSerializationRedisSerializer默认的转换器是SerializingConverter

代码语言:javascript复制
	public JdkSerializationRedisSerializer(@Nullable ClassLoader classLoader) {
		this(new SerializingConverter(), new DeserializingConverter(classLoader));
	}

下面来看一下SerializingConverter的convert方法实现:

代码语言:javascript复制
	@Override
	public byte[] convert(Object source) {
		try  {
		//还有一层:
			return this.serializer.serializeToByteArray(source);
		}
		catch (Throwable ex) {
			throw new SerializationFailedException("Failed to serialize object using "  
					this.serializer.getClass().getSimpleName(), ex);
		}
	}

最终效果如下:

代码语言:javascript复制
	default byte[] serializeToByteArray(T object) throws IOException {
		ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
		serialize(object, out);
		return out.toByteArray();
	}

替换序列化器解决乱码问题

我们需要替换redisTemplate的默认序列化器

上面已经知道了redisTemplate底层默认使用的是jdk序列化器,因此我们最好替换它,String类型可以使用StringRedisSerializer序列化器,底层就是直接调用String.getBytes(),然后对于javabean对象,我们可以使用jackson来进行序列化

我们下面来自定义RedisTemplate的序列化方式:

代码语言:javascript复制
@Configuration
public class RedisConfig {
   @Bean
   public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
   {
       //创建template
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
      //设置连接工厂
       redisTemplate.setConnectionFactory(redisConnectionFactory);
       //设置序列化工具
       GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
       //key和hashKey采用String序列化
       redisTemplate.setKeySerializer(RedisSerializer.string());
       redisTemplate.setHashKeySerializer(RedisSerializer.string());
       //value和hashValue用JSON序列化
       redisTemplate.setValueSerializer(jsonRedisSerializer);
       redisTemplate.setHashValueSerializer(jsonRedisSerializer);
       return redisTemplate;
   }

}

如果你的key也想存对象,那么就不要使用RedisSerializer.string(),转而使用jsonRedisSerializer

测试

我们来测试一下bean对象的存储

代码语言:javascript复制
@SpringBootTest(classes = com.dhy_zk.financialSystem.Main.class)
public class RedisTest {
    @Autowired
    RedisTemplate redisTemplate;

    @Test
    void testString(){
        Peo peo = new Peo("大忽悠", 18);
        redisTemplate.opsForValue().set("大忽悠",peo);
        redisTemplate.opsForValue().get("大忽悠");
    }

}

反序列化时,可以根据@Class字段将JSON字符串转换为指定的对象

Jackson2JsonRedisSerializer底层的反序列源码如下:

代码语言:javascript复制
	public T deserialize(@Nullable byte[] bytes) throws SerializationException {

		if (SerializationUtils.isEmpty(bytes)) {
			return null;
		}
		try {
		//objectMapper相信各位应该都使用过---这里javaType就是从上面的@Class属性解析而来的
			return (T) this.objectMapper.readValue(bytes, 0, bytes.length, javaType);
		} catch (Exception ex) {
			throw new SerializationException("Could not read JSON: "   ex.getMessage(), ex);
		}
	}

额外内存占用如何解决

Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认都是String方式,我们可以直接使用

代码语言:javascript复制
@SpringBootTest(classes = com.dhy_zk.financialSystem.Main.class)
public class RedisTest {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    //JSON
    private static final ObjectMapper mapper=new ObjectMapper();

    @Test
    void testString() throws JsonProcessingException {
      //准备对象
        Peo peo = new Peo("大忽悠", 18);
        //手动序列化
        String string = mapper.writeValueAsString(peo);
        //写入数据到redis
        stringRedisTemplate.opsForValue().set("peo:1",string);

        //读取数据
        String str = stringRedisTemplate.opsForValue().get("peo:1");
        //反序列化
        Peo peo1 = mapper.readValue(str, Peo.class);
    }
}

RedisTemplate的两种序列化实践方案

hashOperations使用说明

代码语言:javascript复制
        HashOperations<String, Object, Object> hashOperations = stringRedisTemplate.opsForHash();
        hashOperations.put("dhy","name","大忽悠");
        hashOperations.put("dhy","age","19");

        Map<Object, Object> eles = hashOperations.entries("dhy");
        System.out.println(eles);

0 人点赞