0. 环境准备
以下是我本机的环境:
SpringBoot2.3.x
Mybatis3.x
Redis5.x
本机或者服务器中搭建好redis环境,并启动成功!这里我用的是阿里云学生机上部署的redis5.x
IDEA2020.x ,Eclipse2020也是可以的,编辑器选择无所谓!
1. 新版本springboot和IDEA编辑器踩坑(可以直接跳过该章节,遇到类似的错误再回头看)
如果您在实操过程中,使用的是IDEA2020 和springboot2.3.x 版本及以上,可能会出现以下和我一样的问题:
问题1:
SpringBoot启动报错: -Property 'configuration' and 'configLocation' can not specified with together
这个问题我头一次碰到,因为我之前一直使用的是 yaml 配置文件中配置 mybatis,如图:
而实操时候我使用SSM的方式,引入mybatis-comfig.xml 全局配置:
所以运行springboot项目时候就报错了!
解决方式:
检查一下application.yaml文件,如果确实是configuration和config-location同时出现在了配置文件中,将configuration配置的内容放入到config-location指向的配置文件中,再次重启项目,文件解决!
建议:
- SpringBoot整合mybatis时,建议将所有mybatis的配置都放入mybatis-config中,这样application.yaml文件内容也会简洁清晰!
问题2
@EnableAutoConfiguration注解报红
这其实是IDEA自动识别的问题,并不是错误,解决方法:
@EnableAutoConfiguration注解的作用
参考文章 @EnableAutoConfiguration注解的作用
同理,如果出现下图问题:
解决方式:
包括其他类似问题(idea 识别报红,可以使用组合功能键ALT+ENTER ),选中如图所示:
取消选中对应复选框即可:
OK,下面我们进入正题!
2. SpringBoot整合Redis作为Mybatis的二级缓存
问题:什么是mybatis的一/二级缓存?
详情请参考文章:浅谈 MyBatis 三级缓存
一级缓存是:sqlSession,sql建立连接到关闭连接的数据缓存
二级缓存是:全局的缓存
2.1 数据库SQL:
CREATE TABLE `score_flow` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id', `score` bigint(19) unsigned NOT NULL COMMENT '用户积分流水', `user_id` int(11) unsigned NOT NULL COMMENT '用户主键id', `user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '用户姓名', PRIMARY KEY (`id`), KEY `idx_userid` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4; CREATE TABLE `sys_user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_name` varchar(11) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户名', `image` varchar(11) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户头像', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8; CREATE TABLE `user_score` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` int(11) unsigned NOT NULL COMMENT '用户ID', `user_score` bigint(19) unsigned NOT NULL COMMENT '用户积分', `name` varchar(30) NOT NULL DEFAULT '' COMMENT '用户姓名', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
2.2 Springboot相关配置:application.yaml
# 端口 server: port: 8080 # 项目访问名称 servlet: context-path: /demo #=====================================数据库相关配置===================================== spring: #=====================================Redis========================================= redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 8.XXXXXX.136 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: cspXXXXXX29 jedis: pool: # 连接池最大连接数(使用负值表示没有限制) max-active: 8 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1 # 连接池中的最大空闲连接 max-idle: 8 # 连接池中的最小空闲连接 min-idle: 0 # 连接超时时间(毫秒) timeout: 8000 #=====================================Mysql========================================= datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: root type: com.alibaba.druid.pool.DruidDataSource # 德鲁伊 minIdle: 5 maxActive: 100 initialSize: 10 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: select 'x' testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 50 removeAbandoned: true filters: stat # ,wall,log4j # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 useGlobalDataSourceStat: true # 合并多个DruidDataSource的监控数据 druidLoginName: wjf # 登录druid的账号 druidPassword: wjf # 登录druid的密码 cachePrepStmts: true # 开启二级缓存 # 开启控制台打印sql日志 mybatis: # 配置mapper文件扫描 mapper-locations: com.haust.redisdemo.mapper/*.xml # 配置实体类扫描 type-aliases-package: com.haust.redisdemo.domain # 指定全局mybatis 配置文件位置 config-location: classpath:/mybatis-config.xml
2.3 mybatis-config.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!-- 打印sql语句 --> <setting name="logImpl" value="STDOUT_LOGGING"/> <!-- 使全局的映射器启用或禁用缓存。 --> <setting name="cacheEnabled" value="true"/> <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 --> <setting name="aggressiveLazyLoading" value="true"/> <!-- 是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) default:true --> <setting name="multipleResultSetsEnabled" value="true"/> <!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true --> <setting name="useColumnLabel" value="true"/> <!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键, 有一些驱动器不兼容不过仍然可以执行。 default:false --> <setting name="useGeneratedKeys" value="false"/> <!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分 FULL:全部 --> <setting name="autoMappingBehavior" value="PARTIAL"/> <!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句; BATCH: 执行器可以重复执行语句和批量更新) --> <setting name="defaultExecutorType" value="SIMPLE"/> <!-- 设置超时时间,它决定驱动等待数据库响应的秒数 --> <setting name="defaultStatementTimeout" value="25"/> <!-- 为驱动的结果集获取数量(fetchSize)设置一个提示值。此参数只可以在查询设置中被覆盖 --> <setting name="defaultFetchSize" value="100"/> <!-- 允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false --> <setting name="safeRowBoundsEnabled" value="false"/> <!-- 使用驼峰命名法转换字段。 --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session --> <setting name="localCacheScope" value="SESSION"/> <!-- 默认为OTHER,为了解决oracle插入null报错的问题要设置为NULL --> <setting name="jdbcTypeForNull" value="NULL"/> <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/> </settings> </configuration>
2.4 主启动类
@SpringBootApplication @EnableAutoConfiguration @MapperScan("com.haust.redisdemo.mapper") public class XdRedisDemoApplication { public static void main(String[] args) { SpringApplication.run(XdRedisDemoApplication.class, args); } }
2.5 User实体类
@Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) /** * 用户实体类 */ public class User implements Serializable {// 必须实现序列化接口! /** * 序列号版本号 */ private static final long serialVersionUID = -4415438719697624729L; /** * 用户id */ private String id; /** * 用户名 */ private String userName; }
2.6 UserMapper.java 与UserMapper.xml
/** * @Auther: csp1999 * @Date: 2020/11/17/10:36 * @Description: UserMapper */ @Repository public interface UserMapper { void insert(User user); void update(User user); void delete(@Param("id") String id); User find(@Param("id") String id); List<User> query(@Param("userName") String userName); void deleteAll(); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.haust.redisdemo.mapper.UserMapper"> <select id="query" resultType="com.haust.redisdemo.domain.User"> select id ,user_name from sys_user where 1=1 <if test="userName != null"> and user_name like CONCAT('%',#{userName},'%') </if> </select> <insert id="insert" parameterType="com.haust.redisdemo.domain.User"> insert sys_user(id,user_name) values(#{id},#{userName}) </insert> <update id="update" parameterType="com.haust.redisdemo.domain.User"> update sys_user set user_name = #{userName} where id=#{id} </update> <delete id="delete" parameterType="string"> delete from sys_user where id= #{id} </delete> <select id="find" resultType="com.haust.redisdemo.domain.User" parameterType="string"> select id,user_name from sys_user where id=#{id} </select> <delete id="deleteAll"> delete from sys_user </delete> </mapper>
2.7 redis操作工具类
这个工具类就是为了操作redis时候相对比较方便而已,其实就是封装了一下RedisTemplete,可以选择不用封装的工具类,直接使用RedisTemplete
/** * @Auther: csp1999 * @Date: 2020/11/17/10:08 * @Description: redis操作工具类 */ @Component public class RedisUtil { @Autowired private RedisTemplate redisTemplate; private static double size = Math.pow(2, 32); /** * 写入缓存 * * @param key * @param offset 位 8Bit=1Byte * @return */ public boolean setBit(String key, long offset, boolean isShow) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.setBit(key, offset, isShow); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 写入缓存 * * @param key * @param offset * @return */ public boolean getBit(String key, long offset) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.getBit(key, offset); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 写入缓存 * * @param key * @param value * @return */ public boolean set(final String key, Object value) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 写入缓存设置时效时间 * * @param key * @param value * @return */ public boolean set(final String key, Object value, Long expireTime) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 批量删除对应的value * * @param keys */ public void remove(final String... keys) { for (String key : keys) { remove(key); } } /** * 删除对应的value * * @param key */ public void remove(final String key) { if (exists(key)) { redisTemplate.delete(key); } } /** * 判断缓存中是否有对应的value * * @param key * @return */ public boolean exists(final String key) { return redisTemplate.hasKey(key); } /** * 读取缓存 * * @param key * @return */ public Object get(final String key) { Object result = null; ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.get(key); return result; } /** * 哈希 添加 * * @param key * @param hashKey * @param value */ public void hmSet(String key, Object hashKey, Object value) { HashOperations<String, Object, Object> hash = redisTemplate.opsForHash(); hash.put(key, hashKey, value); } /** * 哈希获取数据 * * @param key * @param hashKey * @return */ public Object hmGet(String key, Object hashKey) { HashOperations<String, Object, Object> hash = redisTemplate.opsForHash(); return hash.get(key, hashKey); } /** * 列表添加 * * @param k * @param v */ public void lPush(String k, Object v) { ListOperations<String, Object> list = redisTemplate.opsForList(); list.rightPush(k, v); } /** * 列表获取 * * @param k * @param l * @param l1 * @return */ public List<Object> lRange(String k, long l, long l1) { ListOperations<String, Object> list = redisTemplate.opsForList(); return list.range(k, l, l1); } /** * 集合添加 * * @param key * @param value */ public void add(String key, Object value) { SetOperations<String, Object> set = redisTemplate.opsForSet(); set.add(key, value); } /** * 集合获取 * * @param key * @return */ public Set<Object> setMembers(String key) { SetOperations<String, Object> set = redisTemplate.opsForSet(); return set.members(key); } /** * 有序集合添加 * * @param key * @param value * @param scoure */ public void zAdd(String key, Object value, double scoure) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); zset.add(key, value, scoure); } /** * 有序集合获取 * * @param key * @param scoure * @param scoure1 * @return */ public Set<Object> rangeByScore(String key, double scoure, double scoure1) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); redisTemplate.opsForValue(); return zset.rangeByScore(key, scoure, scoure1); } //第一次加载的时候将数据加载到redis中 public void saveDataToRedis(String name) { double index = Math.abs(name.hashCode() % size); long indexLong = new Double(index).longValue(); boolean availableUsers = setBit("availableUsers", indexLong, true); } //第一次加载的时候将数据加载到redis中 public boolean getDataToRedis(String name) { double index = Math.abs(name.hashCode() % size); long indexLong = new Double(index).longValue(); return getBit("availableUsers", indexLong); } /** * 有序集合获取排名 * * @param key 集合名称 * @param value 值 */ public Long zRank(String key, Object value) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); return zset.rank(key, value); } /** * 有序集合获取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start, long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key, start, end); return ret; } /** * 有序集合添加 * * @param key * @param value */ public Double zSetScore(String key, Object value) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); return zset.score(key, value); } /** * 有序集合添加分数 * * @param key * @param value * @param scoure */ public void incrementScore(String key, Object value, double scoure) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); zset.incrementScore(key, value, scoure); } /** * 有序集合获取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start, long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key, start, end); return ret; } /** * 有序集合获取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end); return ret; } }
在RedisConfig中将RedisTemplate 注入IOC容器:
/** * @Auther: csp1999 * @Date: 2020/11/14/18:44 * @Description: Redis 相关配置类 */ @Configuration //@EnableCaching // 开启缓存 public class RedisConfig { /** * 将 redisTemplate 注入IOC * * @param factory * @return */ @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, String> redisTemplate = new RedisTemplate<>(); // RedisTemplate 放入 RedisConnectionFactory 工厂 redisTemplate.setConnectionFactory(factory); return redisTemplate; } }
2.8 在UserController中使用redis缓存
方式一:
/** * @Auther: csp1999 * @Date: 2020/11/17/11:38 * @Description: */ @RestController public class UserController { /** * 缓存盐值:key */ private static final String key = "userCache_"; @Resource private UserMapper userMapper; @Resource private RedisUtil redisUtil; /** * 根据id获取用户信息方式一: * 先从redis缓存查,如果有则取出,如果没有再从数据库查(查到后保存到缓存) * 注意:set值和get值的时候序列化方式必须保持一致 * * @param id * @return */ @RequestMapping("/getUserCache") public User getUseCache(String id) { // step1: 先从redis里面取值 User user = (User) redisUtil.get(key + id); // step2: 如果拿不到则从DB取值 if (user == null) { User userDB = userMapper.find(id); System.out.println("fresh value from DB id:" + id); // step3: DB非空情况刷新redis值 if (userDB != null) { redisUtil.set(key + id, userDB); return userDB; } } return user; } }
假设数据库中已经存在用户信息记录
我们来访问下该接口(第一次访问):
查看控制台打印:
如图,可以看出,第一次根据id查询user信息时候,redis缓存中不存在该user信息,所以直接去数据库中查询!
接下来我们清空控制台打印信息,并刷新一次 http://localhost:8080/demo/getExpire?id=1 链接,模拟第二次访问:
效果如图
从图中得出,未打印sql日志,因此本次访问并未从数据库中获取user信息,而是直接从redis缓存中获取的user信息!
加缓存的好处:学过redis和mysql的应该都知道,MySQL读取的是磁盘中的数据,而redis读取的是内存中的数据(速度快),在大数据量高访问量的情况下,项目后端热点接口不用每次调用都去数据库中查询相关记录,如果缓存中存在相关数据则先从缓存中取,这样会提高了效率!
方式二:
在springboot中提供了简化redis缓存操作的注解:
1、springboot cache的使用:可以结合redis、ehcache等缓存
@CacheConfig(cacheNames=“userInfoCache”) 在同个redis里面必须唯一
@Cacheable(查) :来划分可缓存的方法 - 即,结果存储在缓存中的方法,以便在后续调用(具有相同的参数)时,返回缓存中的值而不必实际执行该方法;
@CachePut(修改、增加):当需要更新缓存而不干扰方法执行时,可以使用@CachePut注释。也就是说,始终执行该方法并将其结果放入缓存中(根据@CachePut选项)
@CacheEvict(删除) : 对于从缓存中删除陈旧或未使用的数据非常有用,指示缓存范围内的驱逐是否需要执行而不仅仅是一个条目驱逐
2、springboot cache的整合步骤:
1)引入pom.xml依赖:
<!-- springboot cache --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
2)RedisConfig开启缓存注解: @EnableCaching
@Configuration @EnableCaching // 开启缓存 public class RedisConfig {
- 3)在方法上面加入SpEL spring的el表达式
UserService.java
/** * User表的增删改查 */ @Service // 本类内方法指定使用缓存时,默认的名称就是userInfoCache @CacheConfig(cacheNames = "userInfoCache") // 开启事务 @Transactional(propagation = Propagation.REQUIRED, readOnly = false, rollbackFor = Exception.class) public class UserService { @Autowired private UserMapper userMapper; /** * 因为必须要有返回值,才能保存到数据库中。 * 如果保存的对象的某些字段是需要数据库生成的, * 那保存对象进数据库的时候,就没必要放到缓存了 * <p> * 数据库中增加某条数据时,缓存中也增加 * * @param user * @return */ // #p0表示第一个参数作为redis中的key // 如果是#p1表示第二个参数作为redis中的key... 这里只有1个参数user // #p0.id 就表示获取user的id作为redis中的key @CachePut(key = "#p0.id") // 必须要有返回值,否则没数据放到缓存中 public User insertUser(User user) { userMapper.insert(user); // user对象中可能只有只几个有效字段,其他字段值靠数据库生成,比如id return userMapper.find(user.getId()); } /** * 当需要更新缓存而不干扰方法执行时可以使用@CachePut注解。 * 也就是说,数据库中对应内容更新时,需要同步更新缓存可使用该注解 * * @param user * @return */ @CachePut(key = "#p0.id") public User updateUser(User user) { userMapper.update(user); // 可能只是更新某几个字段而已,所以查次数据库把数据全部拿出来全部 return userMapper.find(user.getId()); } /** * 使用 @Cacheable注解 会先查询缓存,如果缓存中存在,则不执行查询数据库的方法 * <p> * 使用 springboot cache 默认缓存配置 * * @param id * @return */ @Nullable// 如果可以传入NULL值,则标记为@Nullable,如果不可以,则标注为@Nonnull @Cacheable(key = "#p0") public User findById(String id) { System.err.println("根据id=" + id + "获取用户对象,从数据库中获取"); Assert.notNull(id, "id不用为空"); return userMapper.find(id); } /** * 删除缓存名称为userInfoCache,key等于指定的id对应的缓存 * 数据库中删除某条数据时,缓存中也删除 * * @param id */ @CacheEvict(key = "#p0") public void deleteById(String id) { userMapper.delete(id); } /** * 清空缓存名称为userInfoCache(看类名上的注解)下的所有缓存 * 如果数据失败了,缓存时不会清除的 */ @CacheEvict(allEntries = true) public void deleteAll() { userMapper.deleteAll(); } }
在UserController使用UserService来操作redis缓存:
/** * 根据id获取用户信息方式二: * * userService中加入了springboot cache缓存相关注解 * @param id * @return */ @RequestMapping("/getByCache") public User getByCache(String id) { User user = userService.findById(id); return user; }
以看出方式二,简化了方式一的代码!
方式三:
提问:springboot cache 存在什么问题:
第一:生成key过于简单,例如:userCache::3,容易造成冲突
第二:无法设置过期时间,默认过期时间为永久不过期(如果数据过多且不过期,会造成内存泄漏)
第三:配置序列化方式,默认的是序列化JDKSerialazable
解决方式:
springboot cache 自定义项:
1)自定义KeyGenerator :解决springboot cache默认生成的key过于简单,容易冲突userCache::3问题;
2)自定义cacheManager,设置缓存过期时间:解决springboot cache 默认无法设置过期时间,默认过期时间为永久不过期;
3)自定义序列化方式为,Jackson或者Gson(我们这里使用jackson即可):不适用springboot cache默认序列化方式JDKSerialazable,为什么要更换默认序列号方式呢?因为boot默认的序列化方式可能不支持 日期时间、空值这些变量的序列化,会导致一些错乱乱码问题;
步骤:
1. 在RedisConfig中添加配置:
/** * 自定义KeyGenerator:解决springboot cache默认生成的key过于简单,容易造成重复和冲突的问题 * * @return */ @Bean public KeyGenerator simpleKeyGenerator() { return (o, method, objects) -> {// o:类 method:方法 objects:方法参数 /** * 我们可以使用如下方式(保证唯一性),来自定义KeyGenerator: * 类名 + 方法名 + 参数 * eg: UserInfoList::UserService.findByIdTtl[1] * * 扩展:JVM定位是否是同一个方法的方式 和 这种方式类似 */ StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(o.getClass().getSimpleName()); stringBuilder.append("."); stringBuilder.append(method.getName()); stringBuilder.append("["); for (Object obj : objects) { stringBuilder.append(obj.toString()); } stringBuilder.append("]"); return stringBuilder.toString(); }; } /** * 设置缓存的过期时间 * * @param redisConnectionFactory * @return */ @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { return new RedisCacheManager( RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), // 如果未配置指定的 key 就会使用这个默认策略,过期时间600s this.getRedisCacheConfigurationWithTtl(600), // 如果配置了指定的 key 就会使用指定 key 策略 this.getRedisCacheConfigurationMap() ); } // 指定相应 key 过期时间策略的Map: key:键值 value:缓存过期时间 private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() { Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(); // key为UserInfoList时: 过期时间100s redisCacheConfigurationMap.put("UserInfoList", this.getRedisCacheConfigurationWithTtl(100)); // key为UserInfoListAnother时: 过期时间18000s == 5h redisCacheConfigurationMap.put("UserInfoListAnother", this.getRedisCacheConfigurationWithTtl(18000)); return redisCacheConfigurationMap; } // 指定jackson序列化方式 private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) { Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith( RedisSerializationContext .SerializationPair .fromSerializer(jackson2JsonRedisSerializer) ).entryTtl(Duration.ofSeconds(seconds)); return redisCacheConfiguration; }
在UserService.java中添加方法:
/** * 使用 @Cacheable注解 会先查询缓存,如果缓存中存在,则不执行方法 * <p> * 自定义了 springboot cache 缓存相关的配置 * * @param id * @return */ @Nullable @Cacheable(value = "UserInfoList", keyGenerator = "simpleKeyGenerator") public User findByIdTtl(String id) { // 日志打印 System.err.println("根据id=" + id + "获取用户对象,从数据库中获取"); Assert.notNull(id, "id不用为空"); return userMapper.find(id); }
在controller中使用:
/** * 根据id获取用户信息方式三: * 自定义了 springboot cache 缓存相关的配置 * 有过期时间策略 * 自定义了key: UserInfoList::UserService.findByIdTtl[1] * 自定义序列化方式为jackson * @param id * @return */ @RequestMapping(value = "/getExpire", method = RequestMethod.GET) public User findByIdTtl(String id) { User user = new User(); try { user = userService.findByIdTtl(id); } catch (Exception e) { System.err.println(e.getMessage()); } return user; }
测试访问该接口:
当显示出数据后,说明后端已经从数据库/缓存中读取到了数据,下面我们来看一下redis缓存中对应的key的声明周期:
当redis缓存中key过期后:
我们再次访问该接口查看效果:
由图可得出,这时候redis缓存没有要查询的用户数,这时候是从数据库中查询的!
3. 扩展: redis 面试题
2018支付宝面试题之缓存雪崩:
1、什么是缓存雪崩?你有什么解决方案来防止缓存雪崩?
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU 和内存造成巨大压力,严重的会造成数据库宕机!
2、你有什么解决方案来防止缓存雪崩?
1、加锁排队key: whiltList value:1000w个uid 指定setNx whiltList value nullValue mutex互斥锁解决,Redis的SETNX去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法;
2、数据预热:缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key;
3、双层缓存策略: C1为原始缓存,C2为拷贝缓存,C1失效时,可以访问C2,C1缓存失效时间设置为短期,C2设置为长期;
4、定时更新缓存策略:失效性要求不高的缓存,容器启动初始化加载,采用定时任务更新或移除缓存
5、设置不同的过期时间,让缓存失效的时间点尽量均匀
2018支付宝面试题之缓存穿透:
1、什么是缓存穿透?你有什么解决方案来防止缓存穿透?
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到对应key的value,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库;
2、你有什么解决方案来防止缓存穿透?
1、缓存空值:如果一个查询返回的数据为空(不管是数据不 存在,还是系统故障)我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库;
2、采用布隆过滤器BloomFilter:优势占用内存空间很小,bit存储。性能特别高。 将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个bitmap 拦截掉,从而避免了对底层存储系统的查询压力;
2018支付宝面试题之redis特性:
1、问题1:redis有哪些特性?
1、丰富的数据类型
2、可用于缓存,消息按key设置过期时间,过期后自动删除 setex set expire时间
3、支持持久化方式rdb和aof
4、主从分布式,redis支持主从支持读写分离 redis cluster,动态扩容方式
2、问题2:你用过redis的哪几种特性?
1、用sorted Set实现过排行榜项目
2、用过期key结合springboot cache实现过缓存存储
3、redis实现分布式环境seesion共享
4、用布隆过滤器解决过缓存穿透
5、redis实现分布式锁
6、redis实现订单重推系统
如果文章对您有帮助,点个赞或者点个关注支持下谢谢~