什么是Redis?
早期很多互联网产品在面对高并发时经常出现“响应慢”、“卡住”等用户体验差的情况,那是因为用户的“读”请求远远多于用户的“写”请求,频繁的读请求在高并发的情况下会增加数据库的压力,为了减少用户直接与数据库的交互,许多系统架构引入了缓存中间件,将用户频繁需要读取的数据放入缓存中,可以有效降低数据库的压力。
Redis就是缓存中间件的一种,它是一种基于内存的、采用键值对结构化存储的NoSQL数据库,其底层采用单线程和多路I/O复用模型,所以Redis的查询速度很快。
Redis的应用非常广泛,主要有以下四种应用场景:
- 热点数据的存储于展示
“热点数据”可理解为大部分用户频繁访问的数据,而且这些数据在某一时刻是相同的,比如:微博热搜,新闻头条等等。如果采用查询数据库的方法获取热点数据,将大大增加数据库的压力。 - 最近访问的数据
“最近访问的数据”在数据库中的存储通常以“时间字段”作为标记,采用时间字段与当前时间的“时间差”作比较进行查询,这种方式十分耗时。将“最近访问的数据”存储在Redis的列表List中,将大大减少对数据库的查询压力。 - 并发访问
将某些数据预先存储在Redis中,每次并发过来的请求可以直接从Redis中获取,减少高并发访问给数据库带来的压力。 - 排名
Redis的有序集合zset可以存储需要排序的数据,避免了数据库中Order By等查询方式带来的性能问题。
Redis的基本命令
本文只介绍Redis的基本命令,主要有四种:
- 查看Redis缓存中所有的key:keys *
- 在Redis中创建一个键值对: set key value
- 查看Redis中指定key的值: get key
- 删除Redis中指定的key:del key
在Spring Boot 中使用Redis
在实际项目中很少直接用命令行去操作Redis,而是需要结合实际业务以代码的形式去操作Redis,本文结合Spring Boot去实现操作Redis。
首先,在Spring Boot中加入Redis依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后,在application.properties中加入Redis的连接配置:
spring.redis.host=127.0.0.1
spring.redis.port=6379
使用RedisTemplate操作五种数据结构
字符串String
实战一:将字符串写入缓存中,并读取出来打印在控制台上。
@Test
void TestOne() {
//Redis通用操作组件
ValueOperations vo = redisTemplate.opsForValue();
//把键值对写入Redis中
vo.set("key1", "Redis实战一");
//根据key获取值并打印到控制台
Object object = vo.get("key1");
System.out.println(object);
}
Redis实战一
实战二:将对象序列化为字符串写入缓存,然后反序列化对象并打印在控制台上。
@Data
public class User{
private Integer id;
private String username;
public User(Integer id, String username) {
this.id = id;
this.username = username;
}
}
@Test
void TestTwo() {
User user = new User(1, "测试用户");
//Redis通用操作组件
ValueOperations vo = redisTemplate.opsForValue();
//把键值对写入Redis中
vo.set("key2", objectMapper.writeValueAsString(user));
//根据key获取值并打印到控制台
Object object = vo.get("key2");
System.out.println(object);
}
{"id":1,"username":"测试用户"}
列表List
Redis的列表和Java中List很相似,用于存储一系列具有相同类型的数据。
实战三:将一组已经排好序的用户对象列表存储在缓存中,按照排名的先后顺序获取出来并输出打印到控制台上。
@Test
void TestThree() throws JsonProcessingException {
User user1 = new User(1, "用户1");
User user2 = new User(2, "用户2");
User user3 = new User(3, "用户3");
ListOperations lo = redisTemplate.opsForList();
//把List写入Redis中
lo.leftPush("key3", objectMapper.writeValueAsString(user1));
lo.leftPush("key3", objectMapper.writeValueAsString(user2));
lo.leftPush("key3", objectMapper.writeValueAsString(user3));
//根据key获取值并打印到控制台
int count = Math.toIntExact(lo.size("key3"));
for (int i = 0; i < count; i++) {
Object object = lo.rightPop("key3");
System.out.println(object);
}
}
{"id":1,"username":"用户1"}
{"id":2,"username":"用户2"}
{"id":3,"username":"用户3"}
在实际应用场景中,List类型特别适合“排名”、“排行榜”、“近期访问数据列表”等业务场景。
集合Set
Set用于存储具有相同类型的不重复的数据,即Set中的数据都是唯一的,其底层的数据结构是通过哈希表来实现的,所以其增删改查的复杂度均为O(1)。
实战三:给定一组用户姓名列表,要求剔除具有相同姓名的人员并组成新的集合,存放到缓存中并取出打印到控制台。
void TestFour() {
List<String> list = new ArrayList<>();
list.add("小红");
list.add("小明");
list.add("小王");
list.add("小明");
SetOperations so = redisTemplate.opsForSet();
for (String username: list) {
so.add("key4", username);
}
int count = Math.toIntExact(so.size("key4"));
for (int i = 0; i < count; i++) {
System.out.println(so.pop("key4"));
}
}
小王
小红
小明
从结果可以看出,Set类型可以保证存储的数据唯一但是无序。在实际开发中,Set类型常常用于解决重复提交、剔除重复ID等业务场景。
有序集合Zset
Zset和Set具有某些相同的特性,即存储的数据不重复、无序、唯一的,而这个无序是指集合的存储顺序并不是按照插入顺序来的。而有序集合的有序是指我们可以指定一个属性来排序。
实战5:将手机用户按照充值金额从小到大排序,从大到小排序。
@Test
void TestFive() throws JsonProcessingException {
final String key = "key5";
redisTemplate.delete(key);
List<PhoneUser> phoneUsers = new ArrayList<>();
phoneUsers.add(new PhoneUser(101, 504.0));
phoneUsers.add(new PhoneUser(102, 503.0));
phoneUsers.add(new PhoneUser(103, 502.0));
phoneUsers.add(new PhoneUser(104, 501.0));
ZSetOperations zo = redisTemplate.opsForZSet();
for (PhoneUser phoneUser: phoneUsers) {
zo.add(key, objectMapper.writeValueAsString(phoneUser), phoneUser.getMoney());
}
int count = Math.toIntExact(zo.size(key));
//从小到大排序
System.out.println(zo.range(key, 0L, count));
//从大到小排序
System.out.println(zo.reverseRange(key, 0L, count));
}
[{"phoneNumber":104,"money":501.0}, {"phoneNumber":103,"money":502.0}, {"phoneNumber":102,"money":503.0}, {"phoneNumber":101,"money":504.0}]
[{"phoneNumber":101,"money":504.0}, {"phoneNumber":102,"money":503.0}, {"phoneNumber":103,"money":502.0}, {"phoneNumber":104,"money":501.0}]
默认情况下,Zset会根据充值金额从小到大排序。在实际开发中,Zset充值排行榜、积分排行榜以及成绩排行榜等。
哈希Hash
Hash类型和Java 的HashMap类型有点类似,由key-value键值对组成,而value也有另一个key-value键值对组成,相当于Redis中有个小Redis。
实战六:将学生对象以Hash类型存储,并指定打印某一名学生的信息。
@Data
public class Student implements Serializable {
private Integer id;
private String name;
private Integer age;
public Student() {
}
public Student(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}
@Test
void TestSix() {
final String key = "student";
List<Student> students = new ArrayList<>();
students.add(new Student(1, "小红", 10));
students.add(new Student(2, "小名", 11));
students.add(new Student(3, "小网", 12));
HashOperations ho = redisTemplate.opsForHash();
for (Student student: students) {
ho.put(key, student.getId(), student);
}
Student s1 = (Student) ho.get(key, 3);
System.out.println(s1);
}
Student(id=3, name=小网, age=12)
Hash类型适合存储具有映射关系的类型。在实际开发中,为了减少Key的数量,可以考虑采用Hash存储。
Key失效与判断是否存在
@Test
void TestSeven() throws InterruptedException {
final String key = "seven";
ValueOperations vo = redisTemplate.opsForValue();
vo.set(key, "测试失效", 2, TimeUnit.SECONDS);
System.out.println("是否存在:" + redisTemplate.hasKey(key) + "\t值为:" + vo.get(key));
Thread.sleep(3 * 1000);
System.out.println("是否存在:" + redisTemplate.hasKey(key) + "\t值为:" + vo.get(key));
}
是否存在:true 值为:测试失效
是否存在:false 值为:null
在实际开发中,常见的业务场景包括:
- 将数据库查询到的数据缓存一定的时间TTL,在这段时间内,会从缓存中查询数据;
- 将数据压入缓存队列中,并设置TTL,当时间到,将触发监听事件,从而处理相应的逻辑
Redis大大降低了数据库的压力,但是也带来了一些问题,存在的问题包括:
- 缓存穿透
- 缓存雪崩
- 缓存击穿
缓存穿透
当查询数据库没有查询到数据时,会返回Null,由于Null数据不存入缓存,因此每次访问都要查询数据库,如果有大量的Null数据查询,会给数据库造成极大的压力,这个过程称之为缓存穿透。
目前业界比较成熟的解决方案是:当查询数据库结果为Null时,将Null数据也存储在缓存中,并设置一定的过期时间。
缓存雪崩
缓存雪崩是指在某个时间点,缓存中的Key集体发生“雪崩”式过期,导致大量的查询操作落在了数据库上,导致数据库压力升高。
为了避免这个问题,可以为不同的Key设置不同的过期时间。
缓存击穿
缓存击穿是指当某个频繁被访问的Key,在不停地接受着高并发请求,当这个Key在某个时间点突然失效了,持续的高并发请求“击穿”缓存,导致数据库瞬间压力增加。
为了避免这个问题,可以把这个“热点”Key设置永不过期。