Springboot整合Redis作为Mybatis的二级缓存

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 文章转载声明:转载请附带原文链接

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,如图:


image.png

image.png

而实操时候我使用SSM的方式,引入mybatis-comfig.xml 全局配置:


image.png

所以运行springboot项目时候就报错了!


解决方式:


检查一下application.yaml文件,如果确实是configuration和config-location同时出现在了配置文件中,将configuration配置的内容放入到config-location指向的配置文件中,再次重启项目,文件解决!

image.png

建议

  • SpringBoot整合mybatis时,建议将所有mybatis的配置都放入mybatis-config中,这样application.yaml文件内容也会简洁清晰!

问题2

@EnableAutoConfiguration注解报红

image.png

这其实是IDEA自动识别的问题,并不是错误,解决方法:

image.png

@EnableAutoConfiguration注解的作用

参考文章 @EnableAutoConfiguration注解的作用

同理,如果出现下图问题:

image.png

解决方式:

image.png

包括其他类似问题(idea 识别报红,可以使用组合功能键ALT+ENTER ),选中如图所示:

image.png

取消选中对应复选框即可:

image.png

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;
    }
}


假设数据库中已经存在用户信息记录

image.png

我们来访问下该接口(第一次访问):


image.png


查看控制台打印:image.png

如图,可以看出,第一次根据id查询user信息时候,redis缓存中不存在该user信息,所以直接去数据库中查询!

接下来我们清空控制台打印信息,并刷新一次 http://localhost:8080/demo/getExpire?id=1 链接,模拟第二次访问:

效果如图

image.png

从图中得出,未打印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;
}

测试访问该接口:

image.png

当显示出数据后,说明后端已经从数据库/缓存中读取到了数据,下面我们来看一下redis缓存中对应的key的声明周期:

image.png

当redis缓存中key过期后:

image.png

我们再次访问该接口查看效果:

image.png

由图可得出,这时候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实现订单重推系统

如果文章对您有帮助,点个赞或者点个关注支持下谢谢~


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
1月前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
17天前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
33 4
|
17天前
|
SQL Java 数据库连接
spring和Mybatis的各种查询
Spring 和 MyBatis 的结合使得数据访问层的开发变得更加简洁和高效。通过以上各种查询操作的详细讲解,我们可以看到 MyBatis 在处理简单查询、条件查询、分页查询、联合查询和动态 SQL 查询方面的强大功能。熟练掌握这些操作,可以极大提升开发效率和代码质量。
30 3
|
22天前
|
Java 数据库连接 数据库
spring和Mybatis的逆向工程
通过本文的介绍,我们了解了如何使用Spring和MyBatis进行逆向工程,包括环境配置、MyBatis Generator配置、Spring和MyBatis整合以及业务逻辑的编写。逆向工程极大地提高了开发效率,减少了重复劳动,保证了代码的一致性和可维护性。希望这篇文章能帮助你在项目中高效地使用Spring和MyBatis。
12 1
|
27天前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
37 5
|
1月前
|
NoSQL Java API
springboot项目Redis统计在线用户
通过本文的介绍,您可以在Spring Boot项目中使用Redis实现在线用户统计。通过合理配置Redis和实现用户登录、注销及统计逻辑,您可以高效地管理在线用户。希望本文的详细解释和代码示例能帮助您在实际项目中成功应用这一技术。
32 4
|
1月前
|
消息中间件 NoSQL Java
Spring Boot整合Redis
通过Spring Boot整合Redis,可以显著提升应用的性能和响应速度。在本文中,我们详细介绍了如何配置和使用Redis,包括基本的CRUD操作和具有过期时间的值设置方法。希望本文能帮助你在实际项目中高效地整合和使用Redis。
48 2
|
1月前
|
缓存 NoSQL 中间件
redis高并发缓存中间件总结!
本文档详细介绍了高并发缓存中间件Redis的原理、高级操作及其在电商架构中的应用。通过阿里云的角度,分析了Redis与架构的关系,并展示了无Redis和使用Redis缓存的架构图。文档还涵盖了Redis的基本特性、应用场景、安装部署步骤、配置文件详解、启动和关闭方法、systemctl管理脚本的生成以及日志警告处理等内容。适合初学者和有一定经验的技术人员参考学习。
180 7
|
1月前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
42 0