SpringBoot 應用 Redis 聲明式緩存

Spring 框架提供一種抽象的緩存機制,且 Spring 只提供接口,不提供緩存的具體實現。所以在程序中使用時,需要有具體緩存的實現。目前支持的常見的緩存比如 JDK ConcurrentMap-based Cache、Ehcache、Redis、Caffeine Cache、Guava Cache 等。

所謂聲明式緩存,即使用 Spring 框架提供的註解來使用緩存功能。這就需要知道一些常用的註解:

  • @EnableCaching

Spring 默認沒有開啟緩存註解支持,可以在配置類上使用該註解進行開啟。

  • @Cacheable

程序在執行方法時首先會去緩存中檢查 key 對應的 value 是否存在。如果存在,則方法體不再執行,直接取緩存中的 value 返回;否則執行方法體,並將方法的返回值進行緩存。@Cacheable 註解的使用傾向於減少方法的執行。

  • @CachePut

方法執行完畢之後,將返回值 update 或者 insert 到緩存中。

  • @CacheEvict

方法執行完畢之後,將緩存刪除。

更多關於 Spring 框架緩存註解的說明可以參考官方文檔:Spring Boot Caching

測試環境說明

  • SpringBoot版本:2.0.2.RELEASE
<parent>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-parent/<artifactid>
<version>2.0.2.RELEASE/<version>
<relativepath>
/<parent>
  • Redis 版本:3.2.9,pom 依賴:
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-data-redis/<artifactid>
/<dependency>
  • MySQL 版本:8.0.12,pom 依賴:

<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-data-jpa/<artifactid>
/<dependency>

<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-jdbc/<artifactid>
/<dependency>

<dependency>
<groupid>mysql/<groupid>
<artifactid>mysql-connector-java/<artifactid>
<version>8.0.12/<version>
<scope>runtime/<scope>
/<dependency>
  • mock 數據 sql:
-- 測試數據庫
CREATE DATABASE IF NOT EXISTS test;
-- 城市信息表
CREATE TABLE IF NOT EXISTS `test`.`city_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增 id',
`city` varchar(128) NOT NULL DEFAULT '' COMMENT '城市名稱',
`longitude` bigint(20) NOT NULL DEFAULT '0' COMMENT '經度(100倍存儲)',
`latitude` bigint(20) NOT NULL DEFAULT '0' COMMENT '緯度(100倍存儲)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='城市信息表';

-- fake 測試數據
INSERT INTO `city_info` (`id`, `city`, `longitude`, `latitude`)
VALUES
(1, '合肥', 11717, 3152),
(2, '安慶', 11702, 3031),
(3, '宿州', 11658, 3338);

測試代碼

代碼中已經給出了詳細的註釋說明,且使用方法也比較簡單,不做過多解釋。

Redis 配置

/**
*

Redis 配置


* 繼承 CachingConfigurerSupport 重寫 CacheManager 和 KeyGenerator
* Created by Qinyi.
*/
@EnableCaching // 啟用緩存功能, 默認不啟用
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
private final RedisConnectionFactory redisConnectionFactory;
@Autowired
public RedisConfig(RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
/**
*

配置 CacheManager: 序列化方式、過期時間


* */
@Override
public CacheManager cacheManager() {

// 初始化一個 RedisCacheWriter
// RedisCacheWriter 提供了對 Redis 的 set、setnx、get 等命令的訪問權限
// 可以由多個緩存實現共享,並負責寫/讀來自 Redis 的二進制數據
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// 設置 CacheManager 的值序列化方式
RedisSerializer<object> jsonSerializer = new JdkSerializationRedisSerializer();
RedisSerializationContext.SerializationPair<object> pair = RedisSerializationContext.SerializationPair
.fromSerializer(jsonSerializer);
// 提供 Redis 的配置
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(pair);
// 設置默認超過期時間是 30 秒
defaultCacheConfig.entryTtl(Duration.ofSeconds(30));
// 初始化 RedisCacheManager 返回
return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
}
/**
*

定義 key 生成器: 類名、方法名、參數列表


* 自定義 KeyGenerator 的核心思想是保證 key 不會衝突
* 默認的是 SimpleKeyGenerator, 它使用方法參數組合生成的一個 key, 這裡存在一個問題:
* 如果2個方法, 參數是一樣的. 但執行邏輯不同, 那麼將會導致執行第二個方法時命中第一個方法的緩存. 所以, 通常需要自定義.
* */
@Override
public KeyGenerator keyGenerator() {
return (clazz, method, args) -> {
StringBuilder sb = new StringBuilder();
sb.append(clazz.getClass().getName()).append("#");
sb.append(method.getName()).append("(");
for (Object obj : args) {
sb.append(obj.toString()).append(",");

}
sb.deleteCharAt(sb.length() - 1);
sb.append(")");
return sb.toString();
};
}
}
/<object>/<object>

城市信息實體

/**
*

城市信息實體


* Created by Qinyi.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "city_info")
public class CityInfo implements Serializable {
/** 自增主鍵 */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
/** 城市名稱 */
@Basic
@Column(name = "city", nullable = false)
private String city;
/** 經度 */
@Basic
@Column(name = "longitude", nullable = false)
private Long longitude;
/** 緯度 */
@Basic
@Column(name = "latitude", nullable = false)
private Long latitude;
}

城市信息服務接口定義

/**
*

城市信息服務接口定義


* Created by Qinyi.
*/
public interface ICityInfoService {
/**
*

根據 id 獲取城市信息


* @param id 記錄 id
* @return {@link CityInfo}
* */
CityInfo getCityInfoById(Long id);
/**
*

更新城市信息


* @param newObj 新的城市信息
* @return {@link CityInfo}
* */
CityInfo updateCityInfo(CityInfo newObj);
/**
*

根據 id 刪除城市信息


* @param id 記錄 id
* */
void deleteCityInfoById(Long id);
}

城市信息服務接口定義

/**
*

城市信息服務接口定義


* Created by Qinyi.

*/
@Slf4j
@Service
public class CityInfoServiceImpl implements ICityInfoService {
/** CityInfo Dao */
private final CityInfoRepository repository;
@Autowired
public CityInfoServiceImpl(CityInfoRepository repository) {
this.repository = repository;
}
/**
*

根據 id 獲取城市信息


* 如果不指定 key, 則會使用 KeyGenerator 來生成 key
* 1\\. 如果指定了 key = "#id", redis 中的 key 是 city_info::1
* 2\\. 如果使用自定義的 KeyGenerator(不指定 key), redis 中的 key 是:
* city_info::com.imooc.ad.service.impl.CityInfoServiceImpl#getCityInfoById(1)
* */
@Override
@SuppressWarnings("all")
@Cacheable(cacheNames = "city_info", key = "#id")
// @Cacheable(cacheNames = "city_info")
public CityInfo getCityInfoById(Long id) {
log.info("get CityInfo by id: {}", id.toString());
return repository.findById(id).get();
}
@Override
@CachePut(cacheNames="city_info", key="#newObj.id")
public CityInfo updateCityInfo(CityInfo newObj) {
log.info("update CityInfo: {}", JSON.toJSONString(newObj));
return repository.save(newObj);
}
@Override
@CacheEvict(cacheNames = "city_info", key = "#id")
public void deleteCityInfoById(Long id) {
log.info("delete CityInfo by id: {}", id.toString());
repository.deleteById(id);
}
}
/**
*
* Created by Qinyi.
*/

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class CacheAnnotationTest {
@Autowired
private ICityInfoService cityInfoService;
/**
*

測試多次獲取, 可以直接從緩存(Redis)中獲取數據, 而不用查詢數據庫


* */
@Test
public void testGetCityInfoById() {
System.out.println(JSON.toJSONString(cityInfoService.getCityInfoById(1L)));
}
/**
*

測試更新緩存


* */
@Test
public void testUpdateCityInfo() {
System.out.println(JSON.toJSONString(cityInfoService.updateCityInfo(
new CityInfo(1L, "合肥", 11717L, 3153L)
)));
}
/**
*

測試刪除緩存


* */
@Test
public void testDeleteCityInfoById() {
cityInfoService.deleteCityInfoById(1L);
}
}

執行測試用例之後,可以在 Redis 中看到自動生成的緩存 KV:

127.0.0.1:6379> keys *
1) "city_info::1"
2) "city_info::com.imooc.ad.service.impl.CityInfoServiceImpl#getCityInfoById(1)"

127.0.0.1:6379> type city_info::1
string
127.0.0.1:6379> get city_info::1
"\\\\xac\\\\xed\\\\x00\\\\x05sr\\\\x00\\\\x1ccom.imooc.ad.entity.CityInfo\\\\xe2/O>\\\\xd3\\\\xe6\\\\xee\\\\xc8\\\\x02\\\\x00\\\\x04L\\\\x00\\\\x04cityt\\\\x00\\\\x12Ljava/lang/String;L\\\\x00\\\\x02idt\\\\x00\\\\x10Ljava/lang/Long;L\\\\x00\\blatitudeq\\\\x00~\\\\x00\\\\x02L\\\\x00\\tlongitudeq\\\\x00~\\\\x00\\\\x02xpt\\\\x00\\\\x06\\\\xe5\\\\x90\\\\x88\\\\xe8\\\\x82\\\\xa5sr\\\\x00\\\\x0ejava.lang.Long;\\\\x8b\\\\xe4\\\\x90\\\\xcc\\\\x8f#\\\\xdf\\\\x02\\\\x00\\\\x01J\\\\x00\\\\x05valuexr\\\\x00\\\\x10java.lang.Number\\\\x86\\\\xac\\\\x95\\\\x1d\\\\x0b\\\\x94\\\\xe0\\\\x8b\\\\x02\\\\x00\\\\x00xp\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x01sq\\\\x00~\\\\x00\\\\x05\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x0cPsq\\\\x00~\\\\x00\\\\x05\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00-\\\\xc5"
12

最後

感謝大家的耐心閱讀,喜歡文章的可以關注我,持續為大家推送更多技術乾貨,資料,面試題~

需要面試題和架構資料的可以私信【面試題】或【架構】可獲取Java架構資源合集以及600道面試題+答案。

最後祝福所有遇到瓶疾且不知道怎麼辦的Java程序員們,在往後的工作與面試中一切順利。


分享到:


相關文章: