Redis中间件实战1
Redis各种数据结构实战
字符串
需求:将用户信息存储至缓存中,实现每次前端请求获取用户个人详情时直接从缓存中获取。来演示字符串的写入与读取。 技术方案:为了实现这个需求,首先需要建立用户对象实体,里面包含用户个人的各种信息,包括ID、年龄、姓名、用户名及住址等, 然后采用RedisTemplate操作组件将这一用户对象序列化为字符串信息并写入缓存中,最后从缓存中读取即可。
public class Person implements Serializable {
private int id;
private int age;
private String name;
private String address;
public Person() {
}
public Person(int id, int age, String name, String address) {
this.id = id;
this.age = age;
this.name = name;
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
创建controller
@RestController
@RequestMapping(value = "/string")
public class StringController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper mapper;
private static Logger logger = LoggerFactory.getLogger(StringController.class);
@GetMapping(value = "/one")
public void one() throws Exception{
Person p1 = new Person(95001,23,"孙悟空","花果山水帘洞");
// 定义key与即将存入缓存中的value
final String key = "redis:string:1";
// JSON序列化
String value = mapper.writeValueAsString(p1);
// 写入缓存中
logger.info("存入缓存中的用户实体对象信息为:{}",p1);
//使用set命令写入缓存中
redisTemplate.opsForValue().set(key,value);
// 从缓存中获取用户的信息
final Object object = redisTemplate.opsForValue().get(key);
if (object != null) {
Person p = mapper.readValue(object.toString(),Person.class);
logger.info("从缓存中读取信息:{}",p);
}
}
@GetMapping(value = "/get")
public Person one(String key) throws Exception{
// 从缓存中获取用户的信息
final Object object = redisTemplate.opsForValue().get(key);
if (object != null) {
Person p = mapper.readValue(object.toString(),Person.class);
logger.info("从缓存中读取信息:{}",p);
return p;
}
return null;
}
}
测试 http://localhost:9090/swagger-ui.html
列表
Redis的列表类型跟java的List类型很类似,用于存储一系列具有相同类型的数据。其底层对于数据的存储和读取 可以理解为一个数据队列,往List中添加数据的时候,即相当于往队列中的某个位置插入数据;而从List中 获取数据相当于从队列中某个位置获取数据。
需求: 将一组已经排好序的用户对象列表存储在缓存中,按照排名的先后顺序获取出来并输出到控制台上。 技术方案: 首先需要定义一个已经排好序的用户对象的列表,然后将其存储到Redis的List中,最后按照排名的先后顺序 将每个用户实体获取出来。 代码实现: 创建controller
@RestController
@RequestMapping(value = "/list")
public class ListController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper mapper;
private static Logger logger = LoggerFactory.getLogger(ListController.class);
@GetMapping(value = "/one")
public void one() {
List<Person> pList = new ArrayList<>();
pList.add(new Person(1,20,"孙悟空","花果山水帘洞"));
pList.add(new Person(1,30,"猪八戒","高老庄"));
pList.add(new Person(1,40,"沙悟净","流沙河"));
pList.add(new Person(1,50,"唐三藏","东土大唐"));
logger.info("构造已经排好序的用户对象列表对象:{}",pList);
// 将列表数据存储到Redis中的List中
final String key = "redis:list:1";
ListOperations listOperations = redisTemplate.opsForList();
for (Person p:pList) {
// 往列表中添加数据-从队尾添加
listOperations.leftPush(key,p);
}
// 获取Redis列表中的数据-从队头中遍历获取,直到没有元素为止。
logger.info("获取Redis中list的数据-从队头中获取");
Object res = listOperations.rightPop(key);
Person tmp = null;
while (res != null) {
tmp = (Person)res;
logger.info("当前数据:{}",tmp.getName());
res = listOperations.rightPop(key);
}
}
}
日志
2024-02-05 15:04:20.782 INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController : 构造已经排好序的用户对象列表对象:[com.airycode.redis.bean.Person@4900d96f, com.airycode.redis.bean.Person@2fe9acc6, com.airycode.redis.bean.Person@512f8c42, com.airycode.redis.bean.Person@32385a70]
2024-02-05 15:04:20.823 INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController : 获取Redis中list的数据-从队头中获取
2024-02-05 15:04:20.850 INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController : 当前数据:孙悟空
2024-02-05 15:04:20.853 INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController : 当前数据:猪八戒
2024-02-05 15:04:20.856 INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController : 当前数据:沙悟净
2024-02-05 15:04:20.859 INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController : 当前数据:唐三藏
总结: 在实际的应用场景中,Redis的列表类型特别适用于排名,排行榜,近期访问数据列表等业务场景 是一种很实用的存储类型
集合
Redis的集合类型跟高等数学中学习的集合类似,用于存储具有相同的类型或特性的不重复的数据 ,即Redis中的集合Set存储的数据是唯一的,其底层的数据结构是哈希表,所以添加,删除,查找 的时间复杂度均为O(1)
需求: 给定一组用户姓名列表,要求剔除具有相同姓名的人员并组成新的集合,存放至缓存中并用于前端的访问
解决方案: 首先构造一组用户列表,然后遍历访问,将姓名直接塞入Redis的Set集合中,集合底层会自动剔除重复的元素
核心代码:
@RestController
@RequestMapping(value = "/set")
public class SetController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper mapper;
private static Logger logger = LoggerFactory.getLogger(SetController.class);
@GetMapping("/one")
public void one() throws Exception{
// 构造用户列表
List<Person> list = new ArrayList<>();
list.add(new Person(1,20,"孙悟空","花果山水帘洞"));
list.add(new Person(1,30,"猪八戒","高老庄"));
list.add(new Person(1,40,"沙悟净","流沙河"));
list.add(new Person(1,50,"唐三藏","东土大唐"));
// 待处理的用户列表
logger.info("待处理的用户列表{}",mapper.writeValueAsString(list));
// 遍历访问,剔除相同的姓名的用户并存入集合中,最终存入缓存中
final String key = "redis:set:1";
SetOperations setOperations = redisTemplate.opsForSet();
for (Person p:list) {
setOperations.add(key,p);
}
// 从缓存中获取用户对象集合
Object pop = setOperations.pop(key);
while (pop != null) {
logger.info("从缓存中获取的用户集合-当前用户{}",mapper.writeValueAsString(pop));
pop = setOperations.pop(key);
}
}
}
日志:
待处理的用户列表[{"id":1,"age":20,"name":"孙悟空","address":"花果山水帘洞"},{"id":1,"age":30,"name":"猪八戒","address":"高老庄"},{"id":1,"age":40,"name":"沙悟净","address":"流沙河"},{"id":1,"age":50,"name":"唐三藏","address":"东土大唐"}]
2024-02-05 15:25:45.689 INFO 12484 --- [nio-9090-exec-1] c.a.redis.controller.SetController : 从缓存中获取的用户集合-当前用户{"id":1,"age":50,"name":"唐三藏","address":"东土大唐"}
2024-02-05 15:25:45.693 INFO 12484 --- [nio-9090-exec-1] c.a.redis.controller.SetController : 从缓存中获取的用户集合-当前用户{"id":1,"age":30,"name":"猪八戒","address":"高老庄"}
2024-02-05 15:25:45.696 INFO 12484 --- [nio-9090-exec-1] c.a.redis.controller.SetController : 从缓存中获取的用户集合-当前用户{"id":1,"age":20,"name":"孙悟空","address":"花果山水帘洞"}
2024-02-05 15:25:45.699 INFO 12484 --- [nio-9090-exec-1] c.a.redis.controller.SetController : 从缓存中获取的用户集合-当前用户{"id":1,"age":40,"name":"沙悟净","address":"流沙河"}
总结: Redis的集合类型确实可以保证存储的数据是唯一、不重复的。在实际 的互联网应用中,Redis的Set类型常用于解决重复提交、剔除重复ID等业务场景
有序集合
Redis的有序集合SortedSet跟集合Set具有某些相同的特性,即存储 的数据是不重复、无序、唯一的;而这两者不同之处在于SortedSet可以 通过底层的Score(分数、权重)值对数据进行排序,实际存储的集合数据既不重复又有序 可以说其包含了列表List、集合Set的特性。
需求: 找出一个星期内手机话费单次充值金额前6名的用户列表,要求按照充值的金额从大到小排序,并存入缓存中
技术方案: 首先构造一组对象列表,其对象信息包括用户手机号、充值话费、然后遍历对象,将其加入缓存SortedSet中 最终将其获取出来。
核心代码:
@RestController
@RequestMapping(value = "/sortedset")
public class SortedSetController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper mapper;
private static Logger logger = LoggerFactory.getLogger(SortedSetController.class);
@GetMapping("/one")
public void one() throws Exception{
// 构造用户列表
List<PhoneUser> list = new ArrayList<>();
list.add(new PhoneUser("111",200));
list.add(new PhoneUser("222",100));
list.add(new PhoneUser("333",50));
list.add(new PhoneUser("444",80));
// 待处理的用户列表
logger.info("构造一组无序的用户手机充值对象列表:{}",mapper.writeValueAsString(list));
// 遍历访问,剔除相同的姓名的用户并存入集合中,最终存入缓存中
final String key = "redis:sortedset:1";
final ZSetOperations zSetOperations = redisTemplate.opsForZSet();
for (PhoneUser p:list) {
// 将元素添加进有序集合SortedSet中
zSetOperations.add(key,p,p.getMoney());
}
// 前端获取访问充值排名靠前的用户列表
long size = zSetOperations.size(key);
// 从小到大排序
Set<PhoneUser> resSet = zSetOperations.range(key, 0, size);
// 从大到小排序
// Set resSet = zSetOperations.reverseRange(key, 0, size);
// 遍历获取有序集合中的元素
for (PhoneUser phoneUser:resSet) {
logger.info("从缓存中读取手机充值记录排序列表,当前记录:{}",mapper.writeValueAsString(phoneUser));
}
}
}
日志:
2024-02-05 15:49:29.883 INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController : 构造一组无序的用户手机充值对象列表:[{"phoneNum":"111","money":200},{"phoneNum":"222","money":100},{"phoneNum":"333","money":50},{"phoneNum":"444","money":80}]
2024-02-05 15:49:29.944 INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController : 从缓存中读取手机充值记录排序列表,当前记录:{"phoneNum":"333","money":50}
2024-02-05 15:49:29.944 INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController : 从缓存中读取手机充值记录排序列表,当前记录:{"phoneNum":"444","money":80}
2024-02-05 15:49:29.945 INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController : 从缓存中读取手机充值记录排序列表,当前记录:{"phoneNum":"222","money":100}
2024-02-05 15:49:29.945 INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController : 从缓存中读取手机充值记录排序列表,当前记录:{"phoneNum":"111","money":200}
总结: Redis的有序集合类型SortedSet确实可以实现数据元素的有序排列 默认情况下,SortedSet的排序类型是根据得分Score参数的取值 从小到大排序,如果需要倒序排列,则可以调用reverseRange方法即可
在实际生产环境中,Redis的有序集合SortedSet常用于充值排行榜、积分排行榜、成绩排名等应用场景
哈希存储
Redis的哈希存储跟JAVA的HashMap类似,其底层数据结构是由key-value组成的映射表 ,而其value又是由Field-Value,特别适用于具有映射关系的数据对象的存储。
需求:Redis的哈希数据结构的使用
核心代码:
@RestController
@RequestMapping(value = "/hash")
public class HashController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper mapper;
private static Logger logger = LoggerFactory.getLogger(HashController.class);
@GetMapping("/one")
public void one() throws Exception{
// 构造学生对象列表和水果对象列表
List<Student> students = new ArrayList<>();
List<Fruit> fruits = new ArrayList<>();
// 往学生集合中添加学生对象
students.add(new Student("95001","zs","张三"));
students.add(new Student("95002","ls","李四"));
students.add(new Student("95003","ww","王五"));
// 往水果集合中添加水果对象
fruits.add(new Fruit("苹果","红色"));
fruits.add(new Fruit("橘子","橙色"));
fruits.add(new Fruit("香蕉","黄色"));
// Key
final String sKey = "redis:hashmap:1";
final String fKey = "redis:hashmap:2";
// 获取Hash存储操作组件hashOperations,遍历获取集合中的对象并添加到缓存中
HashOperations hashOperations = redisTemplate.opsForHash();
for (Student s:students) {
hashOperations.put(sKey,s.getId(),s);
}
for (Fruit f:fruits) {
hashOperations.put(fKey,f.getName(),f);
}
// 获取学生对象列表与水果对象列表
Map<String,Student> sMap = hashOperations.entries(sKey);
logger.info("获取学生对象列表:{}",mapper.writeValueAsString(sMap));
Map<String,Fruit> fMap = hashOperations.entries(fKey);
logger.info("获取水果对象列表:{}",mapper.writeValueAsString(fMap));
// 获取指定的学生对象
String sField = "95001";
Student s = (Student) hashOperations.get(sKey,sField);
logger.info("获取指定的学生对象:{}->{}",sField,mapper.writeValueAsString(s));
// 获取指定的水果对象
String fField = "苹果";
Fruit f = (Fruit) hashOperations.get(fKey,fField);
logger.info("获取指定的水果对象:{}->{}",sField,mapper.writeValueAsString(s));
}
}
日志:
2024-02-05 16:37:15.266 INFO 21008 --- [nio-9090-exec-1] c.a.redis.controller.HashController : 获取学生对象列表:{"95003":{"id":"95003","userName":"ww","name":"王五"},"95001":{"id":"95001","userName":"zs","name":"张三"},"95002":{"id":"95002","userName":"ls","name":"李四"}}
2024-02-05 16:37:15.270 INFO 21008 --- [nio-9090-exec-1] c.a.redis.controller.HashController : 获取水果对象列表:{"橘子":{"name":"橘子","color":"橙色"},"香蕉":{"name":"香蕉","color":"黄色"},"苹果":{"name":"苹果","color":"红色"}}
2024-02-05 16:37:15.274 INFO 21008 --- [nio-9090-exec-1] c.a.redis.controller.HashController : 获取指定的学生对象:95001->{"id":"95001","userName":"zs","name":"张三"}
2024-02-05 16:37:15.277 INFO 21008 --- [nio-9090-exec-1] c.a.redis.controller.HashController : 获取指定的水果对象:苹果->{"name":"苹果","color":"红色"}
总结: Redis的Hash存储特别适用于具有映射关系的对象存储。在实际互联网应用,当需要存入缓存中的对象信息具有某种共性时, 为了减少缓存中Key的数量,应考虑采用Hash哈希存储。
Redis中Key失效与判断是否存在
由于Redis本质上是一个基于内存、Key-Value存储的数据库,因而不管采用何种数据类型存储数据时,都需要提供一个Key,称为“键”,用来作为缓存数据的唯一标识。 而获取缓存数据的方法正是通过这个Key来获取到对应的信息,这一点在上面几部分中以代码实战Redis各种数据类型时已有所体现。 然而在某些业务场景下,缓存中的Key对应的数据信息并不需要永久保留,这个时候就需要对缓存中的这些Key进行“清理”。 在Redis缓存体系结构中,Delete与Expire操作都可以用于清理缓存中的Key,这两者不同之处在于Delete操作需要人为手动触发,而Expire只需要提供一个TTL,即“过期时间”,就可以实现Key的自动失效,也就是自动被清理。 下面的代码演示如何使缓存中的Key失效。 方式1:在调用SETEX方法中指定Key的过期时间
@GetMapping("/one")
public void one() throws Exception {
final String key1 = "redis:ttl:1";
ValueOperations valueOperations = redisTemplate.opsForValue();
// 第一种方式:在往redis中set数据时,提供一个TTL,表示ttl时间一到,缓存中的key自动失效,即被清理,在这里ttl是10秒
valueOperations.set(key1, "expire操作", 10L, TimeUnit.SECONDS);
// 等待5秒判断key是否还存在
Thread.sleep(5000);
Boolean existKey1 = redisTemplate.hasKey(key1);
Object value = valueOperations.get(key1);
logger.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey1, value);
//再等待5秒-再判断key是否还存在
Thread.sleep(5000);
existKey1 = redisTemplate.hasKey(key1);
value = valueOperations.get(key1);
logger.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey1, value);
}
日志:
2024-02-06 08:12:29.391 INFO 20940 --- [nio-9090-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-02-06 08:12:29.391 INFO 20940 --- [nio-9090-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2024-02-06 08:12:29.395 INFO 20940 --- [nio-9090-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
2024-02-06 08:12:44.505 INFO 20940 --- [nio-9090-exec-9] c.a.redis.controller.TtlController : 等待5秒-判断key是否还存在:true 对应的值:expire操作
2024-02-06 08:12:49.529 INFO 20940 --- [nio-9090-exec-9] c.a.redis.controller.TtlController : 再等待5秒-再判断key是否还存在:false 对应的值:null
从上述代码运行结果可以看出,当缓存中的Key失效时,对应的值也将不存在,即获取的值为null。
方式2:采用RedisTemplate操作组件的Expire()方法指定失效的Key
@GetMapping("/two")
public void two() throws Exception {
final String key2 = "redis:ttl:2";
ValueOperations valueOperations=redisTemplate.opsForValue();
//第二种方法:在往缓存中set数据后,采用redisTemplate的expire方法使该key失效
valueOperations.set(key2, "expire操作-2");
redisTemplate.expire(key2,10L, TimeUnit.SECONDS);
//等待5秒-判断key是否还存在
Thread.sleep(5000);
Boolean existKey=redisTemplate.hasKey(key2);
Object value=valueOperations.get(key2);
logger.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey, value);
//再等待5秒-再判断key是否还存在
Thread.sleep(5000);
existKey=redisTemplate.hasKey(key2);
value=valueOperations.get(key2);
logger.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey, value);
}
日志:
2024-02-06 08:14:51.708 INFO 20940 --- [nio-9090-exec-2] c.a.redis.controller.TtlController : 等待5秒-判断key是否还存在:true 对应的值:expire操作-2
2024-02-06 08:14:56.715 INFO 20940 --- [nio-9090-exec-2] c.a.redis.controller.TtlController : 再等待5秒-再判断key是否还存在:false 对应的值:null
使缓存中的Key失效与判断 Key是否存在,在实际业务场景中是很常用的,最常见的场景包括:
- 将数据库查询到的数据缓存一定的时间 TTL,在 TTL时间内前端查询访问数据列表时,只需要在缓存中查询即可,从而减轻数据库的查询压力。
- 将数据压入缓存队列中,并设置一定的TTL时间,当TTL时间一到,将触发监听事件,从而处理相应的业务逻辑。
Redis缓存穿透
Redis缓存中间件确实能大大的提高应用程序的整体性能和效率。但是同时也带来了一些问题,比较典型的问题有: 缓存穿透 缓存雪崩 缓存击穿
本篇主要介绍缓存穿透的解决方案
什么是缓存穿透
简而言之:永远越过了缓存直接永远地访问数据库。
说明:
正常情况:
前端用户访问数据,后端首先从redis中查询,如果能查询到数据,则直接返回给用户,
如果缓存中没有数据,则从数据库中查询出来后,直接返回给用户,同时插入到缓存中,
如果在数据库中没有查询到数据,则返回null,同时流程结束。
这个正常查询的流程看上去没有问题,但最后一步"在数据库中没有查询到数据,则返回null,同时流程结束。"
如果前端频繁发起请求,恶意提供数据库中不存在的key,则此时数据库中查询到的数据将永远为null,由于null的数据是不
存入缓存中的,因而每次访问请求时将查询数据库,如果此时有恶意攻击,发起大量的请求,则很有可能对数据库造成极大压力
甚至压垮数据库,这就是缓存穿透
解决方案
当查询数据库时如果没有查询到数据,则将null返回给前端用户,同时将null数据插入到缓存中,并对对应的key设置一定 的过期时间,流程结束。
这种方案:在一定程度上可以减少数据库频繁查询的压力
实战案例
本案例采用springboot+redis,以"商城用户访问热销的商品"为实战案例 演示缓存穿透
步骤1:创建数据库脚本
CREATE TABLE `item` (
`id` bigint(20) NOT NULL,
`code` varchar(255) DEFAULT NULL COMMENT '商品编码',
`name` varchar(255) DEFAULT NULL COMMENT '商品名称',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
步骤2:创建实体类
@Table(name = "item")
public class Item {
private long id;
private String code;
private String name;
/**
* 创建时间
*/
@Column(name = "create_time")
private Date createTime;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
步骤3:创建Mapper
public interface ItemMapper extends Mapper<Item> {
}
步骤4:创建Service
@Service
public class ItemService {
private static final Logger log = LoggerFactory.getLogger(ItemService.class);
@Autowired
private ItemMapper itemMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
private static final String keyPrefix = "item:";
/***
* 获取商品详情,如果缓存有,则从缓存中获取,
* 没有,查询数据库
* @return
* @throws Exception
*/
public Item getItemInfo(String code) throws Exception {
Item item = null;
// 定义缓存中真正的key:由前缀和商品编码组成
final String key = keyPrefix+code;
ValueOperations valueOperations = redisTemplate.opsForValue();
if (redisTemplate.hasKey(key)) {
log.info("获取商品详情-缓存中的该商品编码为:{}"+code);
// 从缓存中获取该商品详情
Object res = valueOperations.get(key);
if (res != null && !"".equals(res.toString())) {
// 如果找到该商品,则进行反序列化解析
item = objectMapper.readValue(res.toString(),Item.class);
}
} else {
// 缓存中没有找到该商品
log.info("获取商品详情-缓存中不存在该商品编码-从数据库中查询商品编码为:{}"+code);
Item param = new Item();
param.setCode(code);
item = itemMapper.selectOne(param);
if (item != null) {
// 如果数据库中查询得到该商品,则将其序列化后写入缓存
valueOperations.set(key,objectMapper.writeValueAsString(item));
} else {
// 过期失效时间设置为5秒,实际情况根据实际业务决定
valueOperations.set(key,"",5, TimeUnit.SECONDS);
}
}
return item;
}
}
步骤5:创建Controller
@RestController
@RequestMapping(value = "/pass")
public class CachePassController {
@Autowired
private ItemService itemService;
@GetMapping("/item/info")
public Item itemInfo(String code) throws Exception{
return itemService.getItemInfo(code);
}
}
体验效果
2024-02-19 15:23:54.047 INFO 18240 --- [nio-9090-exec-2] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:54.239 INFO 18240 --- [nio-9090-exec-3] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:54.430 INFO 18240 --- [nio-9090-exec-6] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:54.629 INFO 18240 --- [nio-9090-exec-5] com.airycode.redis.service.ItemService : 获取商品详情-缓存中不存在该商品编码-从数据库中查询商品编码为:{}95002
2024-02-19 15:23:54.630 DEBUG 18240 --- [nio-9090-exec-5] c.a.redis.mapper.ItemMapper.selectOne : ==> Preparing: SELECT code,name,create_time FROM item WHERE code = ?
2024-02-19 15:23:54.630 DEBUG 18240 --- [nio-9090-exec-5] c.a.redis.mapper.ItemMapper.selectOne : ==> Parameters: 95002(String)
2024-02-19 15:23:54.631 DEBUG 18240 --- [nio-9090-exec-5] c.a.redis.mapper.ItemMapper.selectOne : <== Total: 0
2024-02-19 15:23:54.813 INFO 18240 --- [nio-9090-exec-4] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:58.833 INFO 18240 --- [nio-9090-exec-7] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.014 INFO 18240 --- [io-9090-exec-10] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.181 INFO 18240 --- [nio-9090-exec-9] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.357 INFO 18240 --- [nio-9090-exec-8] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.532 INFO 18240 --- [nio-9090-exec-1] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.701 INFO 18240 --- [nio-9090-exec-2] com.airycode.redis.service.ItemService : 获取商品详情-缓存中不存在该商品编码-从数据库中查询商品编码为:{}95002
2024-02-19 15:23:59.702 DEBUG 18240 --- [nio-9090-exec-2] c.a.redis.mapper.ItemMapper.selectOne : ==> Preparing: SELECT code,name,create_time FROM item WHERE code = ?
2024-02-19 15:23:59.702 DEBUG 18240 --- [nio-9090-exec-2] c.a.redis.mapper.ItemMapper.selectOne : ==> Parameters: 95002(String)
2024-02-19 15:23:59.703 DEBUG 18240 --- [nio-9090-exec-2] c.a.redis.mapper.ItemMapper.selectOne : <== Total: 0
2024-02-19 15:23:59.878 INFO 18240 --- [nio-9090-exec-3] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:00.068 INFO 18240 --- [nio-9090-exec-6] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:03.302 INFO 18240 --- [nio-9090-exec-5] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:03.621 INFO 18240 --- [nio-9090-exec-4] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:03.860 INFO 18240 --- [nio-9090-exec-7] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:11.509 INFO 18240 --- [io-9090-exec-10] com.airycode.redis.service.ItemService : 获取商品详情-缓存中不存在该商品编码-从数据库中查询商品编码为:{}95002
2024-02-19 15:24:11.509 DEBUG 18240 --- [io-9090-exec-10] c.a.redis.mapper.ItemMapper.selectOne : ==> Preparing: SELECT code,name,create_time FROM item WHERE code = ?
2024-02-19 15:24:11.509 DEBUG 18240 --- [io-9090-exec-10] c.a.redis.mapper.ItemMapper.selectOne : ==> Parameters: 95002(String)
2024-02-19 15:24:11.510 DEBUG 18240 --- [io-9090-exec-10] c.a.redis.mapper.ItemMapper.selectOne : <== Total: 0
2024-02-19 15:24:48.297 INFO 18240 --- [nio-9090-exec-9] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95001
2024-02-19 15:29:49.439 INFO 18240 --- [nio-9090-exec-1] com.airycode.redis.service.ItemService : 获取商品详情-缓存中的该商品编码为:{}95001
什么是缓存雪崩
缓存雪崩:指的是在某个时间点,缓存中的Key集体发生过期失效致使大量查询数据库的请求都落在了DB(数据库)上,导致数据库负载过高,压力暴增,甚至有可能“压垮”数据库。
这种问题产生的原因其实主要是因为大量的Key在某个时间点或者某个时间段过期失效导致的。 所以为了更好地避免这种问题的发生,一般的做法是为这些Key设置不同的、随机的TTL(过期失效时间),从而错开缓存中 Key的失效时间点,可以在某种程度上减少数据库的查询压力。
什么是缓存击穿
指缓存中某个频繁被访问的Key(可以称为“热点Key”),在不停地扛着前端的高并发请求,当这个Key突然在某个瞬间过期失效时,持续的高并发访问请求就“穿破”缓存,直接请求数据库,导致数据库压力在某一瞬间暴增。 这种现象就像是“在一张薄膜上凿出了一个洞”。
这个问题产生的原因主要是热点的Key过期失效了,而在实际情况中,既然这个Key可以被当作“热点”频繁访问,那么就应该设置这个Key永不过期,这样前端的高并发请求将几乎永远不会落在数据库上。
总结
不管是缓存穿透、缓存雪崩还是缓存击穿,其实它们最终导致的后果几乎都是一样的, 即给DB(数据库)造成压力,甚至压垮数据库。而它们的解决方案也都有一个共性, 那就是“加强防线”,尽量让高并发的读请求落在缓存中,从而避免直接跟数据库打交道。