项目中我们会经常使用Redis
来缓存一些信息,已减小数据库的压力,增大并发量.平时缓存信息的时候,会经常写这样的代码
以下为伪代码
if (cache != null){
return cache;
}else{
cache = getDataFromDB();
putCacheToRedis(cache);
}
这样就可以简单的为我们缓存数据,如果命中了缓存,那么直接从缓存中获取数据,否则就去数据库中查询数据,然后放入缓存中.我们先不说这种写法存在缓存穿透
、缓存雪崩
、缓存击穿
的风险,单单在各个服务中写相同的代码就已经花费了不少时间.为了减少大量的写重复代码,就可以考虑使用SpringCache
为我们做以上的工作,当然,也可以通过简单的配置和修改为我们解决缓存穿透
、缓存雪崩
、缓存击穿
这些问题.
这里我们使用Springboot
来集成SpringCache
- 首先Maven中引入依赖包(这里引入了
Jedis
连接池)
<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>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
- 然后添加配置
在配置文件中,声明要使用缓存的类型为type
spring:
application:
name: test1
cache:
type: redis
redis:
time-to-live: 3600000 #单位mm
cache-null-values: true #是否缓存空值
redis:
host: 127.0.0.1 # Redis服务器地址
database: 0 # Redis数据库索引(默认为0)
port: 6379 # Redis服务器连接端口
password: yourpassword # Redis服务器连接密码(默认为空)
jedis:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
connect-timeout: 2000
timeout: 2000
server:
port: 8000
接下来,我们编写一个简单的服务,来使用SpringCache
,这里我搭建了一个redisTest1
的模块
![](https://img.haomeiwen.com/i2761682/07bddbac811a9bcd.png)
我粗略的展示一下各个类
Redistest1Application
package com.felix.redistest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
public class Redistest1Application {
public static void main(String[] args) {
SpringApplication.run(Redistest1Application.class, args);
}
}
Test1Service
package com.felix.redistest.service;
import model.TestProduct;
import java.util.List;
public interface Test1Service {
List<TestProduct> updateProducts(Integer count);
List<TestProduct> getAllProducts();
String buyProduct();
}
CacheService
package com.felix.redistest.service;
import model.TestProduct;
import java.util.List;
public interface CacheService {
List<TestProduct> updateProducts(Integer count);
List<TestProduct> getAllProducts();
List<TestProduct> reduceProduct(List<TestProduct> products);
}
Test1ServiceImpl
package com.felix.redistest.service.impl;
import com.felix.redistest.service.CacheService;
import com.felix.redistest.service.Test1Service;
import model.TestProduct;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
public class Test1ServiceImpl implements Test1Service {
@Resource
private CacheService cacheService;
@Resource
private RedissonClient redissonClient;
@Override
public List<TestProduct> updateProducts(Integer count) {
return cacheService.updateProducts(count);
}
@Override
public List<TestProduct> getAllProducts() {
List<TestProduct> products = cacheService.getAllProducts();
System.out.println("获取到商品信息" + new Date().toString());
return products;
}
@Override
public String buyProduct() {
System.out.println(Thread.currentThread().getId());
List<TestProduct> products = getAllProducts();
if (!CollectionUtils.isEmpty(products)){
System.out.println("开始购买商品");
cacheService.reduceProduct(products);
}else{
System.out.println("异常:商品不存在");
}
return "抢购成功";
}
}
CacheServiceImpl
package com.felix.redistest.service.impl;
import com.felix.redistest.service.CacheService;
import model.TestProduct;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
@Service
public class CacheServiceImpl implements CacheService {
@CachePut(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
@Override
public List<TestProduct> updateProducts(Integer count) {
System.out.println("更新商品缓存信息");
return getProductsFromDB(count);
}
@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'",sync = true)
@Override
public List<TestProduct> getAllProducts() {
System.out.println("查询缓存信息" + new Date().toString());
return getProductsFromDB(3);
}
@CachePut(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
@Override
public List<TestProduct> reduceProduct(List<TestProduct> products) {
System.out.println("扣减库存,刷新商品信息");
TestProduct product = products.get(0);
products.remove(0);
product.setStock(product.getStock() - 1);
products.add(0,product);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return products;
}
List<TestProduct> getProductsFromDB(Integer count){
List<TestProduct> products = new ArrayList<>();
Random random = new Random();
for (int i = 0;i < count;i++){
TestProduct product = new TestProduct();
product.setId(Long.parseLong(i + 1 + ""));
product.setName("product" + "-i");
product.setStock(random.nextInt(1000) + 10);
products.add(product);
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return products;
}
}
Test1Controller
package com.felix.redistest.controller;
import com.felix.redistest.service.Test1Service;
import model.TestProduct;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/test")
public class Test1Controller {
@Resource
private Test1Service test1Service;
@GetMapping(value = "/updateCache")
public List<TestProduct> updateCache(Integer count){
return test1Service.updateProducts(count);
}
@GetMapping(value = "/getCache")
public List<TestProduct> getCache(){
return test1Service.getAllProducts();
}
@GetMapping(value = "/buy")
public Object buy(){
String result = test1Service.buyProduct();
return result;
}
}
WSTKRedisConfig
package com.felix.redistest.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
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.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
@EnableConfigurationProperties({CacheProperties.class})
public class WSTKRedisConfig extends CachingConfigurerSupport {
@Autowired
private CacheProperties cacheProperties;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public CacheManager cacheManager() {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
)
);
configuration = configuration.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
);
if (cacheProperties.getRedis().getTimeToLive() != null) {
configuration = configuration.entryTtl(cacheProperties.getRedis().getTimeToLive());
}
if (cacheProperties.getRedis().getKeyPrefix() != null) {
configuration = configuration.prefixCacheNameWith(cacheProperties.getRedis().getKeyPrefix());
}
if (!cacheProperties.getRedis().isCacheNullValues()) {
configuration = configuration.disableCachingNullValues();
}
if (!cacheProperties.getRedis().isUseKeyPrefix()) {
configuration = configuration.disableKeyPrefix();
}
WSTKRedisCacheManager redisCacheManager = new WSTKRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), configuration);
return redisCacheManager;
}
}
WSTKRedisCacheManager
package com.felix.redistest.config;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.util.StringUtils;
import java.time.Duration;
public class WSTKRedisCacheManager extends RedisCacheManager {
public WSTKRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
String[] array = StringUtils.delimitedListToStringArray(name, "#");
name = array[0];
if (array.length > 1) {
long ttl = Long.parseLong(array[1]);
//单位为秒
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
}
return super.createRedisCache(name, cacheConfig);
}
}
逻辑比较简单,分别提供了更新缓存
、获取缓存
、购买商品
三个接口,我们简单分析一下它们的实现
-
WSTKRedisConfig
配置了SpringCache
保存到redis
中使用的序列化工具,如果不自定义,那么会使用JAVA
的序列化工具,在redis
中看到的数据只能无法用于其他编程语言解析
-
WSTKRedisConfig
中注入了自定义的WSTKRedisCacheManager
,这里我们主要是在创建redis
缓存的时候,可以自定义同一类key
的过期时间,使用#
隔开
-
CacheServiceImpl
中的updateProducts
方法上,使用了@CachePut(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
注解,代表我们将会缓存updateProducts
的返回值为value
,key
为PMS_PRODUCT::PRODUCT
,过期时间为120s
,这里的#120
之所以生效,就是使用了我们自定义的WSTKRedisCacheManager
-
CacheServiceImpl
中的getAllProducts
方法上,使用了@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
注解,代表我们将会缓存updateProducts
的返回值为value
,key
为PMS_PRODUCT::PRODUCT
,过期时间为120s
,那么为什么不使用@CachePut
呢?其实从我定义的方法名上就可以看出来了.解释一下,@CachePut
不管缓存中有没有值,都会执行我们的操作,然后存入/覆盖redis
,但是@Cacheable
在命中缓存的话只读取不写入,没有命中缓存才会写入缓存
-
CacheServiceImpl
中的reduceProduct
方法上,使用了@CachePut(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
注解,代表我们将会缓存updateProducts
的返回值为value
,key
为PMS_PRODUCT::PRODUCT
,过期时间为120s
.这里其实跟业务有关,当我们操作了缓存,肯定希望重新更新缓存中的值,所以使用了@CachePut
,当然,我们可以使用组合注解达成目的,比如这里就可以写成
@Caching(
evict = {@CacheEvict(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")},
cacheable = {@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")}
)
这种组合形式,代表我们将会先删除PMS_PRODUCT::PRODUCT
,然后再缓存新的商品信息到PMS_PRODUCT::PRODUCT
中
我们分别来尝试一下,首先访问localhost:8000/test/updateCache?count=3生成三条商品数据,生成会花费10s
的时间
![](https://img.haomeiwen.com/i2761682/a987bc2a490cc50f.png)
当然,如果我们第二次访问该接口,虽然有缓存,但是仍然会重新生成商品数据,并且延迟
10s
才返回数据,刷新了缓存
然后访问[localhost:8000/test/getCache](localhost:8000/test/getCache)
,如果没有命中缓存,那么就会重新花费10s
并存入缓存,否则会直接返回缓存中的数据
![](https://img.haomeiwen.com/i2761682/f63633e1a32c2669.png)
此次请求很快就返回了商品数据信息,可见,第一步的
updateCache
已经生成了缓存并,多次访问localhost:8000/test/getCache得到的数据和之前的一样,代表读取的其实是缓存的数据
再来redis中验证一下
![](https://img.haomeiwen.com/i2761682/4a91d5346fd5af6c.png)
可见数据已经成功缓存了下来
有些朋友可能会注意到,为什么要单独独立出来一个CacheService
呢?不仅仅是为了逻辑更清晰,spring cache的实现原理跟spring的事物管理类似,都是基于AOP的动态代理实现的:即都在方法调用前后 去获取方法的名称、参数、返回值,然后根据方法名称、参数生成缓存的key(自定义的key例外),进行缓存,所以,假如放置updateProducts、getAllProducts这些方法到内部的话,实际上是无法使用缓存的
到此,虽然使用SpringCache
让开发高效了,但是好像也并没有提到解决缓存穿透
、缓存雪崩
、缓存击穿
的问题啊.当然不止于此,我会在下节解释并实际操作,来依次解决这些问题.
以上内容转载请注明出处,同时也请大家不吝你的关注和下面的赞赏
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
网友评论