vlambda博客
学习文章列表

Redis缓存实战记录--上篇

最近一直在做一个rabbitmq的优化,然后又紧接着一个线性池不足的Bug。

决定搭建一层redis作为缓存来优化读取db的速度。

第一次搭建,做个记录

一、基本知识

  • 实体类

  • controller类,用于暴露接口

  • redis template序列化: @configuration @EnableCaching

  • mapper持久层dao: xml配置sql语句

  • service 层: redis模板来写

这里主要是使用RedisTemplate来对远程redis操作,每次访问controller暴露的接口,首先判断redis缓存中是否存在该数据,若不存在就从数据库中读取数据,然后保存到redis缓存中,当下次访问的时候,就直接从缓存中取出来。这样就不用每次都执行sql语句,能够提高访问速度。但是在保存数据到缓存中,通过设置键和值和超时删除,注意设置超时删除缓存时间不要太长,否则会给服务器带来压力。

顺便复习一下springmvc

  • Model:所有的用户数据、状态和程序逻辑,独立于视图和控制器,用来存数据

  • view:呈现模型,类似于web程序的界面

  • controller:控制器,负责获取用户输入信息,进行解析并反馈给模型,通常一个视图有一个controller

复习一下层:

  • dao层:负责与数据库进行一些任务,数据持久层工作 @Repository

  • service层:负责业务模块的业务逻辑应用设计:@Service

    service层业务逻辑有利于通用的业务逻辑的独立性和重复利用性

    • 设计接口

    • 设计实现类

    • 调用已经定义的dao层接口

  • controller层:负责具体的业务模块流程控制/ 调用service层已经设计好的接口来控制业务流程 @Controller

Service层是建立在DAO层之上的,建立了DAO层后才可以建立Service层,而Service层又是在Controller层之下的,因而 Service层应该既调用DAO层的接口,又要提供接口给Controller层的类来进行调用,它刚好处于一个中间层的位置。每个模型都有一个Service接口,每个接口分别封装各自的业务处理方法。

二、springboot整合Redis例子分析

架构:

  • bean:user

  • controller:testcontroller

  • mapper:userdao

  • service:userservice

  • resources:application.yml

一个user表,pom配置

  1. application.yml配置

  2. 实体类:根据user表

  3. controller类:用于暴露接口访问

    • /queryALL /findUserByid /updateUser /delelteUserByid

  4. 配置redistemplate序列化

    • @configuration @EnableCaching

  5. dao层,xml配置sql语句 :@Mapper

  6. service层,使用redis模板来写

    • 先从缓存获取用户,如果没有则取数据库,再写入缓存

    • 先更新数据表,成功之后,删除原来的缓存,再更新缓存

    • 删除数据表中数据,然后删除缓存

三、在Spring中使用Redis

  1. 需要两个包:jedis.jar&spring-data-redis.jar

  2. 使用spring配置连接池:JedisPoolConfig

  3. 配置spring中data redis的连接池工厂:RedisConnectionFactory

  4. 配置完成就可以使用redisTemplate了

  5. 普通的链接没有办法把java对象直接存入Redis,一般是将对象序列化,然后使用redis进行存储:RedisSerializer

  6. 去序列化

  7. 有两个属性:keySerializer/valueSerializer

  8. 为了防止操作是对redis的同一个链接,用SeesionCallback

Redis的一些常用技术

  1. Redis基础事务

    spring中利用SessionCallback接口来进行redis的事务命令

    • multi

    • watch key

    • set/get value

    • exec

    • discard

  2. 探索redis事务回滚

    在命令入队时,就会检测是否正确。否则就会报错。

    3.使用watch监控事务

在multi之前使用watch监控某些键值对

        CAS&乐观锁

watch保持old value

Lettuce学习 :

四个组件:RedisURI, RedisClient,Connection,RedisCommands

public void testSetGet() throws Exception {
   RedisURI redisUri = RedisURI.builder()                    // <1> 创建单机连接的连接信息
          .withHost("localhost")
          .withPort(6379)
          .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
          .build();
   RedisClient redisClient = RedisClient.create(redisUri);   // <2> 创建客户端
   StatefulRedisConnection<String, String> connection = redisClient.connect();     // <3> 创建线程安全的连接
   RedisCommands<String, String> redisCommands = connection.sync();                // <4> 创建同步命令
   SetArgs setArgs = SetArgs.Builder.nx().ex(5);
   String result = redisCommands.set("name", "throwable", setArgs);
   Assertions.assertThat(result).isEqualToIgnoringCase("OK");
   result = redisCommands.get("name");
   Assertions.assertThat(result).isEqualTo("throwable");
   // ... 其他操作
   connection.close();   // <5> 关闭连接
   redisClient.shutdown();  // <6> 关闭客户端
}


四、实际Repo分析

1. redisService.java

  • get()

  • save()

  • clear()

2. redisServiceImpl.java

  • afterPropertiesSet()

    • InitializingBean的唯一方法

    • MeterRegistry:Micrometer实现类,监控指标用(timer、counter)。

  • deserialize() return Map<String, xx>

  • serialize() return String

    • objectMapper.readValue: 把读取的string类型反序列为提供的数据类型

    • objectMapper.writeValueAsString:把该数据写成String类型

    • 对存入redis数据进行序列化和反序列化处理

    • 从ObjectMapper 里面读取

  • clear():清除数据

  • save()

  • StatefulRedisConnection<String,String> :redis 链接


    • sycn() 同步api在所有命令调用之后会立即返回结果

五、Redis 序列化选择

内置序列化方式

  • JackonJsonRedisSerializer

  • JdkSerializationRedisSerializer

  • OxmSerializer

  1. 创建一个POJO对象

  2. 更改applicationConfig

  3. 使用不同序列化方式测试

1.JdkSerializationRedisSerializer

@Test  
public void testJdkSerialiable() {  
   RedisTemplate<String, Serializable> redis = new RedisTemplate<String, Serializable>();  
   redis.setConnectionFactory(connectionFactory);  
   redis.setKeySerializer(ApplicationConfig.StringSerializer.INSTANCE);  
   redis.setValueSerializer(new JdkSerializationRedisSerializer());  
   redis.afterPropertiesSet();  
 
   ValueOperations<String, Serializable> ops = redis.opsForValue();  
 
   User user1 = new User();  
   user1.setUserName("user1");  
   user1.setAge(20);  
 
   String key1 = "users/user1";  
   User user11 = null;  
 
   long begin = System.currentTimeMillis();  
   for (int i = 0; i < 100; i++) {  
       ops.set(key1,user1);  
       user11 = (User)ops.get(key1);  
  }  
   long time = System.currentTimeMillis() - begin;  
   System.out.println("jdk time:"+time);  
   assertThat(user11.getUserName(),is("user1"));  
}

100次存储和获取:jdk time:266

2.JackonJsonRedisSerializer

‘@Test  public void testJacksonSerialiable() {      RedisTemplate<String, Object> redis = new RedisTemplate<String, Object>();      redis.setConnectionFactory(connectionFactory);      redis.setKeySerializer(ApplicationConfig.StringSerializer.INSTANCE);      redis.setValueSerializer(new JacksonJsonRedisSerializer<User>(User.class));      redis.afterPropertiesSet();  

ValueOperations<String, Object> ops = redis.opsForValue();  
 
User user1 = new User();  
user1.setUserName("user1");  
user1.setAge(20);  
 
User user11 = null;  
String key1 = "json/user1";  
 
long begin = System.currentTimeMillis();  
for (int i = 0; i < 100; i++) {  
   ops.set(key1,user1);  
   user11 = (User)ops.get(key1);  
}  
long time = System.currentTimeMillis() - begin;  
 
System.out.println("json time:"+time);  
assertThat(user11.getUserName(),is("user1"));
}

json time:224

3.OxmSerializer

@Test  
public void testOxmSerialiable() throws Throwable{  
   RedisTemplate<String, Object> redis = new RedisTemplate<String, Object>();  
   redis.setConnectionFactory(connectionFactory);  
   redis.setKeySerializer(ApplicationConfig.StringSerializer.INSTANCE);  
 
   redis.setValueSerializer(oxmSerializer);  
   redis.afterPropertiesSet();  
 
   ValueOperations<String, Object> ops = redis.opsForValue();  
 
   User user1 = new User();  
   user1.setUserName("user1");  
   user1.setAge(20);  

   oxm time:335  

从执行时间上来看,JdkSerializationRedisSerializer是最高效的(毕竟是JDK原生的),但是是序列化的结果字符串是最长的。JSON由于其数据格式的紧凑性,序列化的长度是最小的,时间比前者要多一些。而OxmSerialiabler在时间上看是最长的(当时和使用具体的Marshaller有关)。所以个人的选择是倾向使用JacksonJsonRedisSerializer作为POJO的序列器。

我最后选择了JsonSerializer,重点学习一下

4. JsonSerializer

Jackson是MessageConverter消息序列化工具

参考如下博文:https://www.jianshu.com/p/63c5985fb48e

依赖包:jackon-databind, 实际repo中用的jackson-datatype-jsr310

jackson常用注解

官方文档: https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations

  • @JsonProperty 用在属性或者方法上面,用来改变序列化时字段的名字

    public class User {
    @JsonProperty("user_name")
    public String username;
    public String password;
    }
    // 序列化为如下格式username变成了user_name
    {"password":"123456","user_name":"zhangsan"}
  • @JsonIgnoreProperties 用在类上面,用于在序列化时忽略指定字段

    ####@JsonIgnoreProperties({"username", "price"})

    public class Pet {

    private String username;
    private String password;
    private Date birthday;
    private Double price;
    }

    // 指定序列化时忽略username、price字段
    {"password":"123456","birthday":1533887811261}
  • @JsonIgnore 用在属性上面,用于序列化时忽略该属性

public class Pet {

@JsonIgnore
private String username;
private String password;
private Date birthday;
private Double price;
}

// @JsonIgnore加在属性上面,使序列化时忽略该字段
{"password":"123456","birthday":1533888026016,"price":0.6}
  • @JsonFormat 用在Date时间类型属性上面,用于序列化时间为需要的格式

public class Pet {
private String username;
private String password;
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")
private Date birthday;
private Double price;
}

// @JsonFormat加在属性上面,用于jackson对时间格式规定,注意要指定中国时区为+8
{"username":"哈哈","password":"123456","birthday":"2018-08-10 16:17:51","price":0.6}
  • @JsonInclude 用在类上面,用于声明在序列化时忽略一些没有意义的字段,例如:属性为NULL的字段

    @JsonInclude(Include.NON_NULL)
    public class Pet {
    private String username;
    private String password;
    private Date birthday;
    private Double price;
    }

    // @JsonInclude加在类上面,jackson序列化时会忽略无意义的字段,例如username和price是空值,那么就不序列化这两个字段
    {"password":"123456","birthday":1533890045175}
  • @JsonSerialize 用在类或属性上面,用于指定序列化时使用的JsonSerialize类

    public class MyJsonSerializer extends JsonSerializer<Double>{
    @Override
    public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers)
    throws IOException, JsonProcessingException {
    if (value != null)
       gen.writeString(BigDecimal.valueOf(value).
               setScale(2, BigDecimal.ROUND_HALF_UP).toString()); // ROUND_HALF_UP四舍五入
    }
    }

    使用方式:

public class Pet {
private String username;
private String password;
private Date birthday;
@JsonSerialize(using=MyJsonSerializer.class)
private Double price;
}

// 指定序列化price属性时使用自定义MyJsonSerializer,对Double类型进行自定义处理,保留两位小数

{"username":"哈哈","password":"123456","birthday":1533892290795,"price":"0.60"}

Jackson和springboot整合

重写WebMvcConfigurerAdapter类的configureMessageConverters方法即可:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
   @Override
   public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
       ObjectMapper objectMapper = new ObjectMapper();
       // 设置Date类型字段序列化方式
       objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.SIMPLIFIED_CHINESE));
       
       // 指定BigDecimal类型字段使用自定义的CustomDoubleSerialize序列化器
       SimpleModule simpleModule = new SimpleModule();
       simpleModule.addSerializer(BigDecimal.class, new CustomDoubleSerialize());
       objectMapper.registerModule(simpleModule);
       
       MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper);
       converters.add(converter);
  }
}

六、redis消息订阅模式

  1. 注册一个订阅的客户端

    SUBSCRIBE chat

    渠道叫chat,客户端1就会订阅chat渠道的消息了。

  1. 打开另一个客户端

    publish chat 'let's go

    则该客户端向渠道chat发送消息

spring中如何实现redis发布订阅:

  • ..redis.connection.MessageListener接口

  • 实现定义onMessage,实现监听类--序列化转换

    getRedisTemplate().getStringSerializer().deserialize(body);

  • 实现监听容器:RedisMessageListenerContainer:配置线性池 applicationContext.xml

  • 发送消息:redisTemplate.convertAndSend()



与你分享

生活的点点滴滴

长按二维码关注