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配置
application.yml配置
实体类:根据user表
controller类:用于暴露接口访问
/queryALL /findUserByid /updateUser /delelteUserByid
配置redistemplate序列化
@configuration @EnableCaching
dao层,xml配置sql语句 :@Mapper
service层,使用redis模板来写
先从缓存获取用户,如果没有则取数据库,再写入缓存
先更新数据表,成功之后,删除原来的缓存,再更新缓存
删除数据表中数据,然后删除缓存
三、在Spring中使用Redis
需要两个包:jedis.jar&spring-data-redis.jar
使用spring配置连接池:JedisPoolConfig
配置spring中data redis的连接池工厂:RedisConnectionFactory
配置完成就可以使用redisTemplate了
普通的链接没有办法把java对象直接存入Redis,一般是将对象序列化,然后使用redis进行存储:RedisSerializer
去序列化
有两个属性:keySerializer/valueSerializer
为了防止操作是对redis的同一个链接,用SeesionCallback
Redis的一些常用技术
Redis基础事务
spring中利用SessionCallback接口来进行redis的事务命令
multi
watch key
set/get value
exec
discard
探索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
创建一个POJO对象
更改applicationConfig
使用不同序列化方式测试
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消息订阅模式
注册一个订阅的客户端
SUBSCRIBE chat
渠道叫chat,客户端1就会订阅chat渠道的消息了。
打开另一个客户端
publish chat 'let's go
则该客户端向渠道chat发送消息
spring中如何实现redis发布订阅:
..redis.connection.MessageListener接口
实现定义onMessage,实现监听类--序列化转换
getRedisTemplate().getStringSerializer().deserialize(body);
实现监听容器:RedisMessageListenerContainer:配置线性池 applicationContext.xml
发送消息:redisTemplate.convertAndSend()
与你分享
生活的点点滴滴
长按二维码关注