天天看點

Springboot整合Redis作為Mybatis的二級緩存

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

Springboot整合Redis作為Mybatis的二級緩存
Springboot整合Redis作為Mybatis的二級緩存
而實操時候我使用SSM的方式,引入mybatis-comfig.xml 全局配置:
Springboot整合Redis作為Mybatis的二級緩存

是以運作springboot項目時候就報錯了!

解決方式:

檢查一下application.yaml檔案,如果确實是configuration和config-location同時出現在了配置檔案中,将configuration配置的内容放入到config-location指向的配置檔案中,再次重新開機項目,檔案解決!

Springboot整合Redis作為Mybatis的二級緩存
建議:

  • SpringBoot整合mybatis時,建議将所有mybatis的配置都放入mybatis-config中,這樣application.yaml檔案内容也會簡潔清晰!

問題2

@EnableAutoConfiguration注解報紅

Springboot整合Redis作為Mybatis的二級緩存

這其實是IDEA自動識别的問題,并不是錯誤,解決方法:

Springboot整合Redis作為Mybatis的二級緩存

@EnableAutoConfiguration注解的作用

參考文章

同理,如果出現下圖問題:

Springboot整合Redis作為Mybatis的二級緩存
Springboot整合Redis作為Mybatis的二級緩存

包括其他類似問題(idea 識别報紅,可以使用組合功能鍵ALT+ENTER ),選中如圖所示:

Springboot整合Redis作為Mybatis的二級緩存

取消選中對應複選框即可:

Springboot整合Redis作為Mybatis的二級緩存

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

假設資料庫中已經存在使用者資訊記錄

Springboot整合Redis作為Mybatis的二級緩存

我們來通路下該接口(第一次通路):

Springboot整合Redis作為Mybatis的二級緩存

檢視控制台列印:

Springboot整合Redis作為Mybatis的二級緩存

如圖,可以看出,第一次根據id查詢user資訊時候,redis緩存中不存在該user資訊,是以直接去資料庫中查詢!

接下來我們清空控制台列印資訊,并重新整理一次 http://localhost:8080/demo/getExpire?id=1 連結,模拟第二次通路:

效果如圖

Springboot整合Redis作為Mybatis的二級緩存

從圖中得出,未列印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;
}
      

測試通路該接口:

Springboot整合Redis作為Mybatis的二級緩存

當顯示出資料後,說明後端已經從資料庫/緩存中讀取到了資料,下面我們來看一下redis緩存中對應的key的聲明周期:

Springboot整合Redis作為Mybatis的二級緩存

當redis緩存中key過期後:

Springboot整合Redis作為Mybatis的二級緩存

我們再次通路該接口檢視效果:

Springboot整合Redis作為Mybatis的二級緩存

由圖可得出,這時候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實作訂單重推系統

如果文章對您有幫助,點個贊或者點個關注支援下謝謝~