1.1 Redis的介绍
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
1.2 Redis的配置
步骤:
-
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
编写配置
#Redis配置 spring.redis.host=127.0.0.1 spring.redis.port=6379
-
编写自定义配置
@Configuration public class CommonConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public RedisTemplate<String,Object> redisTemplate(){ //定义RedisTemplate实例 RedisTemplate<String,Object> redisTemplate = new RedisTemplate<String,Object>(); //设置Redis的链接工厂 redisTemplate.setConnectionFactory(redisConnectionFactory); //指定大Key序列化策略为String序列化,Value为JDK自带的序列化策略 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); //指定Hashkey序列化策略为String序列化(针对Hash存储) redisTemplate.setHashKeySerializer(new StringRedisSerializer()); return redisTemplate; } @Bean public StringRedisTemplate stringRedisTemplate(){ //采用默认配置即可 StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); stringRedisTemplate.setConnectionFactory(redisConnectionFactory); return stringRedisTemplate; } }
-
测试
-
采用RedisTemplate将一字符串信息写入缓存中,并读取输出到控制台
配置
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.30</version> <scope>test</scope> </dependency>
代码
//单元测试 @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest class RedisApplicationTests { //定义日志 private static final Logger log = LoggerFactory.getLogger(RedisApplicationTests.class); @Autowired private RedisTemplate redisTemplate; @Test void contextLoads() { log.info("-----开始RedisTemplate操作组件实战-----"); final String content = "RedisTemplate 实战字符串信息"; final String key = "redis:template:string"; //Redis通过的操作组件 ValueOperations valueOperations = redisTemplate.opsForValue(); log.info("写入缓存中的内容:{}",content); valueOperations.set(key,content); //从缓存中读取 Object result = valueOperations.get(key); log.info("读取来的内容:{}",result); } }
结果
-
采用RedisTemplate将一对象信息序列化为JSON格式字符串后写入缓存中,然后将其读取出来,最后反序列化解析其中的内容并输出到控制台。
class RedisApplicationTests { //定义日志 private static final Logger log = LoggerFactory.getLogger(RedisApplicationTests.class); //定义RedisTemplate操作组件 @Autowired private RedisTemplate redisTemplate; //定义JSON序列化与反序列化框架类 @Autowired private ObjectMapper objectMapper; @Test void test() throws JsonProcessingException { log.info("-----开始RedisTemplate操作组件实战-----"); //构造对象信息 User user = new User(1,"debug","小A"); //Redis通用的操作组件 ValueOperations valueOperations = redisTemplate.opsForValue(); //将序列化后的信息写入缓存中 final String key = "redis:template:test:object"; final String value = objectMapper.writeValueAsString(user); valueOperations.set(key,value); log.info("写入缓存对象的信息:{}",user); //从缓存中读取内容 Object result = valueOperations.get(key); if(result!=null){ User resultUser = objectMapper.readValue(result.toString(),User.class); log.info("读取缓存内容并反序列化后的结果:{}",resultUser); } } }
-
StringRedisTemplate和RedisTemplate的操作基本一样,只是将RedisTemplate操作组件改成StringRedisTemplate操作组件
1.3 Redis常见数据类型
1.3.1 字符串
业务场景:将用户个人信息存储至缓存,实现每次前端请求获取用户个人详情信息时直接从缓存中读取。为了实现这个需求,首先需要建立用户对象实体,里面包含用户的个人信息,包括id、年龄、姓名、用户名以及住址等等,然后采用RedisTemplate操作组件将这一用户对象序列化为字符串信息并写入缓存,最后从缓存中读取即可。
class RedisApplicationTests {
//定义日志
private static final Logger log = LoggerFactory.getLogger(RedisApplicationTests.class);
//定义RedisTemplate操作组件
@Autowired
private RedisTemplate redisTemplate;
//定义JSON序列化与反序列化框架类
@Autowired
private ObjectMapper objectMapper;
@Test
void test() throws JsonProcessingException {
log.info("-----开始RedisTemplate操作组件实战-----");
//构造对象信息
User user = new User(1,"debug","小A");
//Redis通用的操作组件
ValueOperations valueOperations = redisTemplate.opsForValue();
//将序列化后的信息写入缓存中
final String key = "redis:template:test:object";
final String value = objectMapper.writeValueAsString(user);
valueOperations.set(key,value);
log.info("写入缓存对象的信息:{}",user);
//从缓存中读取内容
Object result = valueOperations.get(key);
if(result!=null){
User resultUser = objectMapper.readValue(result.toString(),User.class);
log.info("读取缓存内容并反序列化后的结果:{}",resultUser);
}
}
}
1.3.2 列表
Redis列表的底层对于数据的存储和读取可以理解为一个“数据队列”,往列表中添加数据时,相当于往队列中的某个位置添加数据(如从队头添加数据);而从列表获取数据时,相当于从队列中的某个位置中获取数据(如从队尾获取数据)。
业务场景:将一组已经排好序的用户对象列表存储在缓存中,并按照排名的先后获取出来输出到控制台。对于这样的需求,首先需要定义一个已经排好序的用户对象列表,然后将其存储到Redis的列表中,最后按照排名先后按每个用户实体对象获取出来。
@Test
void listTest(){
//构造一组用户姓名列表
List<String> userList = new ArrayList<>();
userList.add("debug");
userList.add("jack");
userList.add("修罗");
userList.add("大圣");
userList.add("jack");
userList.add("修罗");
userList.add("大圣");
log.info("待处理的用户姓名列表:{}",userList);
//将list的数据存入缓存,redis集合中的数据时不重复的。
final String key = "redis:test:list";
SetOperations setOperations = redisTemplate.opsForSet();
for( String item:userList){
setOperations.add(key,item);
}
//从缓存中获取用户对象集合
Object result = setOperations.pop(key);
while(result!=null){
log.info("从缓存中获取的用户集合--当前用户:{}",result);
result = setOperations.pop(key);
}
}
1.3.3 有序集合
Redis的有序集合跟集合具有某些相同的特性,即存储的数据是不重复、无序和唯一的。而这两者的不同之处在于有序集合可以通过底层的Score(分数/权重)值对数据进行排序,实现存储的集合数据即不重复又有序,可以说其包含了列表、集合的特性。
业务场景:找出一个星期内手机话费单次充值金额前6的用户列表,要求按照充值金额从大到小的顺序进行排序,并存储至缓存中。
为了实现这一需求,首先需要构造一组对象列表,其中对象信息包括用户手机号、充值金额,然后需要遍历缓存的有序集合中,最终将其获取出来。
@Test
void zSetTest(){
//构造一组无序的用户手机充值对象列表
List<PhoneUser> list = new ArrayList<>();
list.add(new PhoneUser("103",130.0));
list.add(new PhoneUser("101",120.0));
list.add(new PhoneUser("102",80.0));
list.add(new PhoneUser("105",70.0));
list.add(new PhoneUser("106",50.0));
list.add(new PhoneUser("104",150.0));
log.info("构造一组无序的用户手机充值对象列表:{}",list);
final String key = "redis:test:zsettest";
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
for( PhoneUser u:list){
//将元素添加进有序集合中
zSetOperations.add(key,u,u.getFare());
}
//前端获取访问充值排名靠前的用户列表
Long size = zSetOperations.size(key);
//从大到小排序
Set<PhoneUser> resSet = zSetOperations.reverseRange(key,0,size);
for (PhoneUser u:resSet){
log.info("从缓存中读取手机充值记录排序列表,当前列表:{}",u);
}
}
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class PhoneUser implements Serializable {
private String phone;
private Double fare;
//手机号重复,代表充值记录重复(只适用于特殊的排名需要),所以需要重写equals()和hashCode()方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PhoneUser phoneUser = (PhoneUser) o;
return phone!=null?Objects.equals(phone, phoneUser.phone):Objects.equals(phoneUser.phone,null);
}
@Override
public int hashCode() {
return phone!=null?phone.hashCode():0;
}
}
要缓存的JavaBean必须实现Serializable接口,因为Spring会将对象先序列化再存入 Redis,或者使用JSON序列化JavaBean
1.3.4 Key失效与判断是否存在
在某些业务场景下,缓存中的Key对应的数据信息并不需要永久保留,这个时候我们就需要对缓存中的这些Key进行清理,在Redis缓存体系结构中,Delete与Expire操作都可以用于清理缓存中的Key,这两者不同之处在于Delete操作需要人为手动触发,而Exprie只需要提供一个TTL,即过期时间,就可以实现Key的自动失效,也就是过期的Key自动被清理,这有两种设置方法。
第一种方法:在调用set方法中指定Key的过期时间。
@Test
void testTTL1() throws Exception {
final String key1 = "redis:testttl1";
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(key1,"exprie操作",10L, TimeUnit.SECONDS);
Thread.sleep(5000);
Boolean existKey = redisTemplate.hasKey(key1);
Object value = valueOperations.get(key1);
log.info("等待5秒-判断key是否还存在:{},对应的值:{}",existKey,value);
Thread.sleep(5000);
existKey = redisTemplate.hasKey(key1);
value = valueOperations.get(key1);
log.info("等待10秒-判断key是否还存在:{},对应的值:{}",existKey,value);
}
第两个方法:采用RedisTemplate操作组件的Exprie方法。
@Test
void testTTL12() throws Exception {
final String key2 = "redis:testttl2";
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(key2,"exprie操作");
redisTemplate.expire(key2,10L,TimeUnit.SECONDS);
Thread.sleep(5000);
Boolean existKey = redisTemplate.hasKey(key2);
Object value = valueOperations.get(key2);
log.info("等待5秒-判断key是否还存在:{},对应的值:{}",existKey,value);
Thread.sleep(5000);
existKey = redisTemplate.hasKey(key2);
value = valueOperations.get(key2);
log.info("等待10秒-判断key是否还存在:{},对应的值:{}",existKey,value);
}
1.4 Redis实战——缓存穿透
1.4.1 缓存穿透与解决方案
顾名思义,就是永远穿越了缓存而直接访问数据库。
将从数据库中获得的null结果直接存入缓存,那么下次就直接从缓存中获取null结果。
1.4.2 实战过程
步骤:
-
构建SQL表
-
使用Mybatis逆向工程生成xml、mapper和model
-
编写Controller
-
编写Service
主要的业务在此,逻辑是先判断redis是否有对应key,若无则从数据库读取,并放入redis缓存中,**注意:无论数据库中有无数据都需要放入缓存中,否则会发生缓存穿透