写文一方面是为了自己复习面试,另外就是想让大家和我一起卷吧(哈哈说笑了,是希望大家有所收获吧)
我也不敢说什么一文详解本地缓存和分布式缓存,本文就是针对事实,用图文并茂的方式,可运行的代码案例结合常见的面试题,一点点的推导和分析发生的问题。
文章中针对可能会出现的问题,都附带了一副彩图,来让大家对问题具有更深刻的理解记忆。
希望各位小伙伴都能够满载而归~
全文大致9000字上下,并且具有推导性,所以在看本文前,建议能腾出较多的空闲时间,摸鱼看的话,可能会略微有一点点割裂感,可以先收藏,哈哈~
文章大纲:
一、缓存
1、缓存的基本知识
1)什么是缓存?
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而DB只承担数据落盘工作。
最简单的理解就是缓存是挡在 DB 前面的一层,为DB遮风挡雨。
架构中最经典的一句话:“没有什么是加一层不能解决的,如果加一层不行,就再加一层”。
2)什么样的数据适合放入缓存?
精简为四字就是:读多写少
- 访问量很大,需要使用缓存来承担一部分压力(读多、写少)
- 即时性要求高,能承受一定时间内的数据不一致性。
- 较长时间不会改变的数据,如后台管理的菜单列表,商品分类列表等等。
3)使用缓存后会产生什么样的问题?
- 缓存与数据库双写不一致
- 缓存雪崩、缓存穿透
- 缓存并发竞争
4)缓存的使用流程
这只是一个非常简单的流程介绍,实际中还是有不少值得思考的地方。
2、使用Map模拟本地缓存
所谓的缓存,其实就是一个位于应用程序与数据库之间的一层,作用就是减少访问数据库的次数,以提高服务性能。
单机服务下,一些较小,并且是单线程中用到的到数据,使用本地 Map 来存储也不是不可以。
如果是学习过 ThreadLocal
的小伙伴,就可能见过ThreadLocalMap
这个Map
,一般而言,ThreadLocal
都是用来存储本次请求中一些信息(例如:当前请求中登录用户信息),方便在整个请求过程中使用,不过往往它都是一次性的~
我下面的案例只是简单的模拟一下本地缓存,并不实用,为解释大致的含义而写。
/** * <p> * 分类菜单 服务实现类 * </p> * * @author Ning Zaichun * @since 2022-09-07 */ @Slf4j @Service public class LocalMenuServiceImpl implements ILocalMenuService { /** * 本地缓存 * 最开始的话,拿 HashMap 模拟 * 但 HashMap 它是一个非线程安全类集合, * 进一步又改为使用 ConcurrentHashMap,多线程下安全的集合 */ private Map<String, Object> localCacheMap = new ConcurrentHashMap<String, Object>(); private static final String LOCAL_MENU_CACHE_KEY = "local:menu:list"; @Autowired private MenuMapper menuMapper; @Override public List<MenuEntity> getLocalList() { //1、判断本地缓存中是否存在 List<MenuEntity> menuEntityList = (List<MenuEntity>) localCacheMap.get(LOCAL_MENU_CACHE_KEY); //2、本地缓存中有,就从缓存中拿 if (menuEntityList == null) { //3、如果缓存中没有,就重新查询数据库 log.info("缓存中没有,查询数据库,重新构建缓存"); menuEntityList = menuMapper.selectList(new QueryWrapper<MenuEntity>()); //4、从数据库查询到结果后,重新放入缓存中 localCacheMap.put(LOCAL_MENU_CACHE_KEY, menuEntityList); return menuEntityList; } log.info("缓存中有直接返回"); //5、将结果返回 return menuEntityList; } /** * 更新操作 * * @param menu * @return */ @Override public Boolean updateLocalMenuById(MenuEntity menu) { //1、删除本地缓存数据 localCacheMap.remove(LOCAL_MENU_CACHE_KEY); System.out.println("清空本地缓存===>"); //2、更新数据库,根据id更新数据库中实体信息 return menuMapper.updateById(menu) > 0; } }
问题:
并不实用,存在较多问题,存储数据量较小,并发能力较弱等等。
软件架构中一直流传着这么一句话:
"没有什么是加一层解决不了的,如果加一层解决不了,就再加一层"。
所以就将缓存抽取出来,架构演变成如下图:
二、集成 Redis 做缓存 and Redis 的使用
这里准确点说应当是集成 Redis 做编程式的缓存,而非大家常见的集成 Spring-Cache
利用注解做缓存。
我从上至下大致会说到的下列几个知识点:
- Redis 的简单使用
- Redis 序列化机制
- 更改 Redis 默认序列化机制
- 使用 Redis 做编程式的缓存
- 简单讲解了 Redis 的两个连接工厂
Jedis
和Lettuce
2.1、前期准备
添加 Redis 的相关依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
使用SpringBoot这么久以来,看到某个stater,可以放心的推测,它大概率会有一个xxxxAutoConfiguration
的自动配置类
在这里可以看到它给我们自动注入了RedisTemplate
和StringRedisTemplate
两个常用的操作模板类。
配置yml文件:
spring: application: name: springboot-cache redis: host: 192.168.1.1 password: 000415 #有就写,木有则省略
2.2、Redis 的简单使用
在说其他的之前,我们先来看看 Redis 常用的一些命令,从浅到深
/** * @description: * @author: Ning Zaichun * @date: 2022年09月21日 22:01 */ @RunWith(SpringRunner.class) @SpringBootTest(classes = {ApplicationCache.class}) public class RedisTest { @Autowired private StringRedisTemplate stringRedisTemplate; /** * set key value 命令 使用 */ @Test public void test1() { // set key value 往redis 中set 一个值 stringRedisTemplate.opsForValue().set("username", "宁在春"); // get key : 从redis中根据key 获取一个值 System.out.println(stringRedisTemplate.opsForValue().get("username")); //out:宁在春 // del key: 从redis 中删除某个key stringRedisTemplate.delete("username"); System.out.println(stringRedisTemplate.opsForValue().get("username")); } /** * setnx key value : 如果 key 不存在,则设置成功 返回 1 ;否则失败 返回 0 */ @Test public void test2() { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1"); System.out.println(lock); Boolean lock2 = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1"); System.out.println(lock2); //true //false } /** * set key value nx ex : 如果 key 不存在,则设置值和过期时间 返回 1 ;否则失败 返回 0 * nx、ex 都为命令后面的参数 * 更为详细命令的解释:https://redis.io/commands/set/ * 这个命令也常常在分布式锁中出现,悄悄为后文铺垫一下 */ @Test public void test3() { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock:nx:ex", "lock", 30L, TimeUnit.SECONDS); System.out.println(lock); Boolean lock2 = stringRedisTemplate.opsForValue().setIfAbsent("lock:nx:ex", "lock", 30L, TimeUnit.SECONDS); System.out.println(lock2); //true //false } /** * 上述的三个测试,都是基础的 set 命令的,但 redis 中也有很多其他的数据结构,如 list、set、map 等等 * list测试,可将列表视为队列(先进先出) * 想要详细了解,请查看:https://redis.io/docs/data-types/lists/ * 【注意注意注意:还有很多方法没有测试,大家私下可以多用用】 */ @Test public void test4() { ListOperations<String, String> opsForList = stringRedisTemplate.opsForList(); //从列表左边进行插入 opsForList.leftPush("left:key", "1"); opsForList.leftPush("left:key", "2"); opsForList.leftPush("left:key", "3"); opsForList.leftPush("left:key", "4"); opsForList.leftPush("left:key", "5"); //从列表右边进行插入 opsForList.rightPush("left:key", "10"); opsForList.rightPush("left:key", "9"); opsForList.rightPush("left:key", "8"); opsForList.rightPush("left:key", "7"); opsForList.rightPush("left:key", "6"); //按规定范围取出数据 ,也可以取出单个数据 leftPop rightPop ,按这样的方式取出来,也就代表从列表中删除了。 List<String> stringList = opsForList.range("left:key", 1, 10); stringList.forEach(System.out::print); //out::4321109876 } /** * set 测试,set 数据结构是唯一字符串(成员)的无序集合 * 想要详细了解,请查看:https://redis.io/docs/data-types/sets/ * 【注意注意注意:还有很多方法没有测试,大家私下可以多用用】 */ @Test public void test5() { SetOperations<String, String> opsForSet = stringRedisTemplate.opsForSet(); opsForSet.add("set:key","宁在春","1","1","2","3","开始学习"); Set<String> members = opsForSet.members("set:key"); members.forEach(System.out::print); // 3宁在春21开始学习 System.out.println(""); // 从 set 集合中删除 value 为 1 的值 opsForSet.remove("set:key","1"); Set<String> members2 = opsForSet.members("set:key"); members2.forEach(System.out::print); //2宁在春3开始学习 // 注意:取出来的时候,不一定是插入顺序 } /** * Redis 哈希 测试, * Redis 哈希是结构为字段值对集合的记录类型。您可以使用散列来表示基本对象并存储计数器分组等。 * 想要详细了解,请查看:https://redis.io/docs/data-types/hashes/ * 【注意注意注意:还有很多方法没有测试,大家私下可以多用用】 * 应用场景:可以存储登录用户的相关信息 */ @Test public void test6() { HashOperations<String, Object, Object> opsForHash = stringRedisTemplate.opsForHash(); opsForHash.put("hash:key","username","宁在春"); opsForHash.put("hash:key","school","xxxx学校"); opsForHash.put("hash:key","age","3"); Object username = opsForHash.get("hash:key", "username"); System.out.println(username); //宁在春 username="宁在春写的这篇文章还不错,值得一赞"; // 更新某一个值数据 opsForHash.put("hash:key","username",username); Object username2 = opsForHash.get("hash:key", "username"); System.out.println(username2); //宁在春写的这篇文章还不错,值得一赞 // 删除某一条数据 opsForHash.delete("hash:key","age"); // 展示某个key下所有的 hashKey Set<Object> keys = opsForHash.keys("hash:key"); keys.forEach(System.out::println); //username //school // 展示某个key下所有 hashKey 对应 Value值 List<Object> hashKeyValues = opsForHash.values("hash:key"); hashKeyValues.forEach(System.out::println); //宁在春写的这篇文章还不错,值得一赞 //xxxx学校 HashMap<String, String> map = new HashMap<>(); map.put("name","nzc"); map.put("address","china"); opsForHash.putAll("hash:key2",map); List<Object> values = opsForHash.values("hash:key2"); values.forEach(System.out::println); //china //nzc } @Autowired private RedisTemplate<Object,Object> redisTemplate; /** * 上面的操作都是操作字符串,但是大家在使用的过程中都知道,我们大都是存放到Redis中的数据,都是将某个对象直接放入Redis中 * * redisTemplate 的操作和 stringRedisTemplate 的操作都是一样的,只是内部存放的不同罢了 */ @Test public void test7() { Map<String, Student> map = new HashMap<>(); Student s1 = new Student(); s1.setSchool("xxxx1"); s1.setUsername("ningzaichun1号"); s1.setAge(3); Student s2 = new Student(); s2.setSchool("xxxx2"); s2.setUsername("ningzaichun2号"); s2.setAge(5); map.put("user:1",s1); map.put("user:2",s2); redisTemplate.opsForHash().putAll("student:key",map); List<Object> values = redisTemplate.opsForHash().values("student:key"); values.forEach(System.out::println); //Student(username=ningzaichun1号, school=xxxx1, age=3) //Student(username=ningzaichun2号, school=xxxx2, age=5) } /** * 虽然大家可以在Java 程序中看到取出来的值是正常的, * 但是在平时开发和测试的时候,我们还是需要借助 Redis 的可视化工具来查看的, * 你会发现,我们采用默认的序列化机制(JDK序列化机制),在Redis可视化软件中,会无法直接查看的, * 都是转码之后的数据: \xac\xed\x00\x05t\x00\x06user:2 * 图片见下面第二张图 */ @Test public void test8() { Map<String, Student> map = new HashMap<>(); Student s1 = new Student(); s1.setSchool("xxxx1"); s1.setUsername("ningzaichun1号"); s1.setAge(3); Student s2 = new Student(); s2.setSchool("xxxx2"); s2.setUsername("ningzaichun2号"); s2.setAge(5); map.put("user:1",s1); map.put("user:2",s2); redisTemplate.opsForHash().putAll("student:key",map); List<Object> values = redisTemplate.opsForHash().values("student:key"); values.forEach(System.out::println); //Student(username=ningzaichun1号, school=xxxx1, age=3) //Student(username=ningzaichun2号, school=xxxx2, age=5) } }
list
数据类型演示 leftPush
和 rightPush
命令时的效果图:
使用 Redis 默认序列化机制,往Redis中存储对象的结果展示:
可以看到在可视化界面中,他们的值都被序列化成这么一串字符串了,不便于数据的查看。
将 Redis 的默认序列化机制,改成什么样的才合适呢?
改成 JSON
格式的序列化机制最佳,既方便查看,也支持各种各样的程序。
聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的思考2:https://developer.aliyun.com/article/1394670