一、缓存概念知识 1、是什么缓存 我们日常生活中,经常会接触听到缓存这个词,例如,浏览器清空缓存,处理器缓存大小,磁盘缓存等等。经过分类,可以将缓存分为:
硬件缓存: 一般指的是机器上的 CPU、硬盘等等组件的缓存区间,一般是利用的内存作为一块中转区域,都通过内存交互信息,减少系统负载,提供传输效率。
客户端缓存: 一般指的是某些应用,例如浏览器、手机App、视频缓冲等等,都是在加载一次数据后将数据临时存储到本地,当再次访问时候先检查本地缓存中是否存在,存在就不必去远程重新拉取,而是直接读取缓存数据,这样来减少远端服务器压力和加快载入速度。
服务端缓存: 一般指远端服务器上,考虑到客户端请求量多,某些数据请求量大,这些热点数据经常要到数据库中读取数据,给数据库造成压力,还有就是 IO、网络等原因有一定延迟,响应客户端较慢。所以,在一些不考虑实时性的数据中,经常将这些数据存在内存中(内存速度非常快),当请求时候,能够直接读取内存中的数据及时响应。
2、为什么使用缓存 用缓存,主要有解决 高性能
与 高并发
与 减少数据库压力
。缓存本质就是将数据存储在内存中,当数据没有发生本质变化的时候,我们应尽量避免直接连接数据库进行查询,因为并发高时很可能会将数据库压塌,而是应去缓存中读取数据,只有缓存中未查找到时再去数据库中查询,这样就大大降低了数据库的读写次数,增加系统的性能和能提供的并发量。
3、缓存的优缺点 优点:
加快了响应速度
减少了对数据库的读操作,数据库的压力降低。
缺点:
内存容量相对硬盘小。
缓存中的数据可能与数据库中数据不一致。
因为内存断电就清空数据,存放到内存中的数据可能丢失。
二、Redis 概念知识 1、什么是 Redis Redis 是一个高性能的 Key-Value
数据库,它是完全开源免费的,而且 Redis 是一个 NoSQL
类型数据库,是为了解决 高并发
、高扩展
,大数据存储
等一系列的问题而产生的数据库解决方案,是一个非关系型的数据库。但是,它也是不能替代关系型数据库,只能作为特定环境下的扩充。
2、为什么使用 Redis 作为缓存
支持高可用: Redis 支持 master\slave 主\从机制、sentinal 哨兵模式、cluster 集群模式,这样大大保证了 Redis 运行的稳定和高可用行。
支持多种数据结构: Redis 不仅仅支持简单的 Key/Value 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储。
支持数据持久化: 可以将内存中的数据持久化在磁盘中,当宕机或者故障重启时,可以再次加载进如 Redis,从而不会或减少数据的丢失。
有很多工具与插件对其支持: Redis 已经在业界广泛使用,已经是成为缓存的首选目标,所以很多语言和工具对其支持,我们只需要简单的操作就可以轻松使用。
3、Redis 支持的数据类型 Redis 支持的数据结构类型包括:
字符串(string)
哈希表(hash)
列表(list)
集合(set)
有序集合(zset)
为了保证读取的效率,Redis 把数据对象都存储在内存当中,它可以支持周期性的把更新的数据写入磁盘文件中。而且它还提供了交集和并集,以及一些不同方式排序的操作。
三、缓存后可能遇见的问题 1、缓存穿透
缓存穿透: 指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
缓存穿透几种解决办法:
缓存空值,在从 DB 查询对象为空时,也要将空值存入缓存,具体的值需要使用特殊的标识, 能和真正缓存的数据区分开,另外将其过期时间设为较短时间。
使用布隆过滤器,布隆过滤器能判断一个 key 一定不存在(不保证一定存在,因为布隆过滤器结构原因,不能删除,但是旧值可能被新值替换,而将旧值删除后它可能依旧判断其可能存在),在缓存的基础上,构建布隆过滤器数据结构,在布隆过滤器中存储对应的 key,如果存在,则说明 key 对应的值为空。
2、缓存击穿
缓存击穿: 某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
缓存击穿几种解决办法:
设置二级缓存,或者设置热点缓存永不过期,需要根据实际情况进行配置。
使用互斥锁,在执行过程中,如果缓存过期,那么先获取分布式锁,再执行从数据库中加载数据。如果找到数据就存入缓存,没有就继续该有的动作,在这个过程中能保证只有一个线程操作数据库,避免了对数据库的大量请求。
3、缓存雪崩
缓存雪崩: 当缓存服务器重启、或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力,造成数据库后端故障,从而引起应用服务器雪崩。
缓存雪崩几种解决办法:
缓存组件设计高可用,缓存高可用是指,存储缓存的组件的高可用,能够防止单点故障、机器故障、机房宕机等一系列问题。例如 Redis sentinel 和 Redis Cluster,都实现了高可用。
请求限流与服务熔断降级机制,限制服务请求次数,当服务不可用时快速熔断降级。
设置缓存过期时间一定的随机分布,避免集中在同一时间缓存失效。
定时更新缓存策略,对于实时性要求不高的数据,定时进行更新。
4、缓存一致性 使用缓存很大可能导致数据不一致问题,如下:
更熟数据库成功 -> 更新缓存失败 -> 数据不一致
更新缓存成功 -> 更新数据库失败 -> 数据不一致
更新数据库成功 -> 淘汰缓存失败 -> 数据不一致
淘汰缓存成功 -> 更新数据库失败 -> 查询缓存mis
所以使用缓存时候,应该结合实际情况,考虑缓存的数据是否有一致性需求。
四、SpringBoot 如何结合 Redis 实现缓存 1、Mavne 引入相关依赖
spring-boot-starter-data-redis :
commons-pool2 :
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > </dependency >
2、配置 Redis 参数 application 文件中添加连接 Redis 的配置参数
1 2 3 4 5 6 7 8 9 10 11 12 13 spring: redis: host: 127.0 .0 .1 port: 6379 database: 0 timeout: 1000 password: 123456 lettuce: pool: max-active: 20 max-wait: -1 min-idle: 0 max-idle: 10
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 spring: redis: sentinel: master: "my-master" nodes: "192.168.2.11:6379,192.168.2.12:6379,192.168.2.13:6379" database: 0 timeout: 1000 password: 123456 lettuce: pool: max-active: 20 max-wait: -1 min-idle: 0 max-idle: 10
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: redis: cluster: max-redirects: 5 nodes: "192.168.2.11:6379,192.168.2.12:6379,192.168.2.13:6379" database: 0 timeout: 1000 password: 123456 lettuce: pool: max-active: 20 max-wait: -1 min-idle: 0 max-idle: 10
3、配置 Spring 缓存管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Configuration public class RedisConfig { @Bean public CacheManager cacheManager (RedisConnectionFactory factory) { RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10 )) .prefixKeysWith("cache:user:" ) .disableCachingNullValues() .serializeKeysWith(keyPair()) .serializeValuesWith(valuePair()); return RedisCacheManager.builder(factory).withCacheConfiguration("user" , cacheConfig).build(); } private RedisSerializationContext.SerializationPair<String> keyPair () { return RedisSerializationContext.SerializationPair.fromSerializer( new StringRedisSerializer () ); } private RedisSerializationContext.SerializationPair<Object> valuePair () { return RedisSerializationContext.SerializationPair.fromSerializer( new GenericJackson2JsonRedisSerializer () ); } }
4、服务中使用 SpringCache 的注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Service @CacheConfig(cacheNames = "user") public class UserServiceImpl implements UserService { public User addUser (User user) { ...... } @Cacheable(key = "#username") public User getUserByUsername (String username) { ...... } @CachePut(key = "#user.username") public User updateUser (User user) { ...... } @CacheEvict(key = "#username") public void deleteByUsername (String username) { ...... } @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) public Book importBooks (String deposit, Date date) }
注解说明:
@CacheConfig: 一般配置在类上,指定缓存名称,cacheNames的值是和上面“缓存管理器”中缓存名称的一致。
@Cacheable: 作用于方法上,用于对于方法返回结果进行缓存,如果已经存在该缓存,则直接从缓存中获取,缓存的key可以从入参中指定,缓存的 value 为方法返回值。
@CachePut: 作用于方法上,无论是否存在该缓存,每次都会重新添加缓存,缓存的key可以从入参中指定,缓存的value为方法返回值,常用作于更新。
@CacheEvict: 作用于方法上,用于清除缓存
@Caching: @Caching允许在同一方法上使用多个嵌套的@Cacheable,@CachePut和@CacheEvict
上面注解中的常用配置参数:
value: 缓存管理器中配置的缓存的名称,这里可以理解为一个组的概念,缓存管理器中可以有多套缓存配置,每套都有一个名称,类似于组名,这个可以配置这个值,选择使用哪个缓存的名称,配置后就会应用那个缓存名称对应的配置。
key: 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合。
condition: 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存。
unless: 不缓存的条件,和 condition 一样,也是 SpEL 编写,返回 true 或者 false,为 true 时则不进行缓存。
5、启动类添加开启缓存注解 1 2 3 4 5 6 7 8 9 @EnableCaching @SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
@EnableCaching: 作用于类上,用于开启注解功能。
五、SpringCache 操作缓存的不足 使用 Spring Cache 虽然方便,但是也有很多局限性,因为它多是根据请求参数命名 key,根据返回指设置 value,这样很多情况下,我们想方法内部进行命名和操作有一定的限制。如果我们需要灵活设置缓存,可以不用 SpringCache 提供的注解,直接在代码中使用 Spring-data-redis 包提供的方法,手动操作 key 与 value。
opsForValue().set(String key, String value);
opsForValue().get(String key);
1 2 3 4 5 6 7 8 9 10 @Autowired private RedisTemplate<String, Object> redisTemplate;public void redisBatch () { redisTemplate.opsForValue().set("key" , "value" ); redisTemplate.opsForValue().get("key" ); }
还有经常要批量设置、读取缓存,可以使用:
opsForValue().multiSet(Map map);
opsForValue().multiGet(List list);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Autowired private RedisTemplate<String, Object> redisTemplate;public void redisBatch () { Map<String,Object> map = new HashMap <>(); map.put("test1" ,"value1" ); map.put("test2" ,"value2" ); map.put("test3" ,"value3" ); redisTemplate.opsForValue().multiSet(map); List<String> list = new ArrayList <>(); list.add("test1" ); list.add("test2" ); list.add("test3" ); List<Object> valueList = redisTemplate.opsForValue().multiGet(list); }
六、SpringBoot + SpringCache + Redis 示例项目 下面是一个简单的 SpringBoot 项目,用于对用户的增删改查,这里使用 SpringCache 来模拟对数据进行缓存,示例如下:
1、Mavne 引入相关依赖 Maven 中引入 SpringBoot 和 Redis 依赖,因为使用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.2.2.RELEASE</version > </parent > <groupId > mydlq.club</groupId > <artifactId > springboot-redis-example</artifactId > <version > 0.0.1</version > <name > springboot-redis-example</name > <description > Demo project for Spring Boot Redis</description > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
2、配置连接 Redis 参数 1 2 3 4 5 6 7 8 9 10 11 12 13 spring: redis: host: 127.0 .0 .1 port: 6379 database: 0 timeout: 1000 password: lettuce: pool: max-active: 20 max-wait: -1 min-idle: 0 max-idle: 10
3、配置 Spring 缓存管理器 缓存配置类,里面配置缓存管理器,配置缓存的全局过期时间、序列化等参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import org.springframework.cache.CacheManager;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.cache.RedisCacheConfiguration;import org.springframework.data.redis.cache.RedisCacheManager;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.serializer.*;import java.time.Duration;@Configuration public class RedisConfig { @Bean public CacheManager cacheManager (RedisConnectionFactory factory) { RedisCacheConfiguration cacheConfig1 = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10 )) .prefixKeysWith("cache:user:" ) .disableCachingNullValues() .serializeKeysWith(keyPair()) .serializeValuesWith(valuePair()); RedisCacheConfiguration cacheConfig2 = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(30 )) .prefixKeysWith("cache:user_info:" ) .disableCachingNullValues() .serializeKeysWith(keyPair()) .serializeValuesWith(valuePair()); return RedisCacheManager.builder(factory) .withCacheConfiguration("user" , cacheConfig1) .withCacheConfiguration("userInfo" , cacheConfig2) .build(); } private RedisSerializationContext.SerializationPair<String> keyPair () { return RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer ()); } private RedisSerializationContext.SerializationPair<Object> valuePair () { return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer ()); } }
4、定义实体类 用户实体类
User
1 2 3 4 5 6 7 8 9 10 11 12 13 import com.fasterxml.jackson.annotation.JsonProperty;import lombok.Data;@Data public class User { private String username; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; private String role; }
用户信息实体类
UserInfo
1 2 3 4 5 6 7 8 9 10 11 import lombok.Data;@Data public class UserInfo { private String name; private String sex; private Integer age; }
5、定义服务接口 UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import mydlq.club.example.entity.User;public interface UserService { void addUser (User user) ; User getUserByUsername (String username) ; User updateUser (User user) ; void deleteByUsername (String username) ; }
UserInfoService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import mydlq.club.example.entity.UserInfo;public interface UserInfoService { void addUserInfo (UserInfo userInfo) ; UserInfo getByName (String name) ; UserInfo updateUserInfo (UserInfo userInfo) ; void deleteByName (String name) ; }
6、实现服务类 实现 UserService 与 UserInfoService 接口中的方法,里面使用 @Cacheable
、@CachePut
、@CacheEvict
三个注解完成对用户与用户信息数据的缓存。
UserServiceImpl(用户业务实现类)
注意,为了演示方便,没有连接数据库,临时创建了个成员变量 userMap 来模拟数据库存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import mydlq.club.example.entity.User;import mydlq.club.example.service.UserService;import org.springframework.beans.BeanUtils;import org.springframework.cache.annotation.CacheConfig;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.CachePut;import org.springframework.cache.annotation.Cacheable;import org.springframework.stereotype.Service;import java.util.HashMap;@Service @CacheConfig(cacheNames = "user") public class UserServiceImpl implements UserService { private HashMap<String, User> userMap = new HashMap <>(); @Override public void addUser (User user) { userMap.put(user.getUsername(), user); } @Override @Cacheable(key = "#username",unless = "#result==null ") public User getUserByUsername (String username) { if (!userMap.containsKey(username)) { return null ; } return userMap.get(username); } @Override @CachePut(key = "#user.username") public User updateUser (User user) { if (!userMap.containsKey(user.getUsername())){ throw new RuntimeException ("不存在该用户" ); } User newUser = userMap.get(user.getUsername()); BeanUtils.copyProperties(user, newUser, "username" ); userMap.put(newUser.getUsername(), newUser); return newUser; } @Override @CacheEvict(key = "#username") public void deleteByUsername (String username) { userMap.remove(username); } }
UserInfoServiceImpl(用户信息业务实现)
注意,为了演示方便,没有连接数据库,临时创建了个成员变量 userInfoMap 来模拟数据库存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import mydlq.club.example.entity.UserInfo;import mydlq.club.example.service.UserInfoService;import org.springframework.beans.BeanUtils;import org.springframework.cache.annotation.CacheConfig;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.CachePut;import org.springframework.cache.annotation.Cacheable;import org.springframework.stereotype.Service;import java.util.HashMap;@Service @CacheConfig(cacheNames = "userInfo") public class UserInfoServiceImpl implements UserInfoService { private HashMap<String, UserInfo> userInfoMap = new HashMap <>(); @Override public void addUserInfo (UserInfo userInfo) { userInfoMap.put(userInfo.getName(), userInfo); } @Override @Cacheable(key = "#name", unless = "#result==null") public UserInfo getByName (String name) { if (!userInfoMap.containsKey(name)) { return null ; } return userInfoMap.get(name); } @Override @CachePut(key = "#userInfo.name") public UserInfo updateUserInfo (UserInfo userInfo) { if (!userInfoMap.containsKey(userInfo.getName())) { throw new RuntimeException ("该用户信息没有找到" ); } UserInfo newUserInfo = userInfoMap.get(userInfo.getName()); BeanUtils.copyProperties(userInfo, newUserInfo, "name" ); userInfoMap.put(newUserInfo.getName(), newUserInfo); return newUserInfo; } @Override @CacheEvict(key = "#name") public void deleteByName (String name) { userInfoMap.remove(name); } }
7、创建 Controller UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import mydlq.club.example.entity.User;import mydlq.club.example.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;@RestController public class UserController { @Autowired private UserService userService; @GetMapping("/user/{username}") public User getUser (@PathVariable String username) { return userService.getUserByUsername(username); } @PostMapping("/user") public String createUser (@RequestBody User user) { userService.addUser(user); return "SUCCESS" ; } @PutMapping("/user") public User updateUser (@RequestBody User user) { return userService.updateUser(user); } @DeleteMapping("/user/{username}") public String deleteUser (@PathVariable String username) { userService.deleteByUsername(username); return "SUCCESS" ; } }
UserInfoController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import mydlq.club.example.entity.UserInfo;import mydlq.club.example.service.UserInfoService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;@RestController public class UserInfoController { @Autowired private UserInfoService userInfoService; @GetMapping("/userInfo/{name}") public UserInfo getUserInfo (@PathVariable String name) { return userInfoService.getByName(name); } @PostMapping("/userInfo") public String createUserInfo (@RequestBody UserInfo userInfo) { userInfoService.addUserInfo(userInfo); return "SUCCESS" ; } @PutMapping("/userInfo") public UserInfo updateUserInfo (@RequestBody UserInfo userInfo) { return userInfoService.updateUserInfo(userInfo); } @DeleteMapping("/userInfo/{name}") public String deleteUserInfo (@PathVariable String name) { userInfoService.deleteByName(name); return "SUCCESS" ; } }
8、启动类 启动类中添加 @EnableCaching
注解开启缓存。
1 2 3 4 5 6 7 8 9 10 11 12 import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@EnableCaching @SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
博文示例项目 Github 地址: https://github.com/my-dlq/blog-example/tree/master/springboot/springboot-redis-cache-example
转自:http://www.mydlq.club/article/55/