缓存:cache
public Result queryById(Long id) {
//根据id在redis中查询数据
String s = redisTemplate.opsForValue().get(CACHE_SHOP_KEY +id);
//判断是否存在
if (!StrUtil.isBlank(s)) {
//将字符串转为bean
//存在,直接返回
Shop shop = JSONUtil.toBean(s, Shop.class);
return Result.ok(shop);
}
//不存在,查询数据库
Shop shop = getById(id);
if (shop==null){
//不存在,返回404
return Result.fail("店铺不存在");
}
//数据库中是否存在,存在则写入缓存,并返回
redisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
JSONUtil.toBean(s, Shop.class);
JSONUtil.toJsonStr(shop);
缓存更新策略:
先删除数据库后删除缓存的线程安全可能性低。
缓存穿透:
1.查询店铺在数据库和redis中都不存在时,写入空值到redis中
2.查询数据为空值时,直接返回不要查询数据库。
public Result queryById(Long id) { //根据id在redis中查询数据 String s = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY +id); //判断是否存在 if (!StrUtil.isBlank(s)) { //将字符串转为bean //存在,直接返回 Shop shop = JSONUtil.toBean(s, Shop.class); return Result.ok(shop); } //判断是否店铺是否存在,缓存中的空数据 if (s!=null){ //返回空值 return Result.fail("店铺信息不存在"); } //不存在,查询数据库 Shop shop = getById(id); if (shop==null){ //不存在,返回404 //缓存空值到数据库中 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("店铺不存在"); } //数据库中是否存在,存在则写入缓存,并返回 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop); } |
互斥锁解决缓存击穿:
使用redis中的setnx实现互斥锁,如果key不存在则创建,存在则无法创建。
//使用redis中的setnx实现互斥锁。
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
//潜在的NullReferenceException,如果装箱的对象是null,那么在拆箱时可能会抛出NullReferenceException
return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
拆箱和装箱操作可能会产生的问题:
1.性能开销:装箱和拆箱涉及内存分配和复制,这会增加额外的性能开销。
2.类型安全丢失:装箱操作会将值类型包装在一个对象中,这样原本在栈上的值类型数据现在位于堆中,可能导致类型安全检查丢失。
3.垃圾回收压力:装箱操作会创建新的对象实例,这可能增加垃圾回收器的压力,尤其是在频繁进行装箱和拆箱操作的情况下。
4.潜在的NullReferenceException,如果装箱的对象是null,那么在拆箱时可能会抛出NullReferenceException异常。
/**
* 互斥锁解决缓存击穿
* @param id
* @return
*/
private Shop queryWithMutex(Long id){
//根据id在redis中查询数据
String s = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY +id);
//判断是否存在
if (!StrUtil.isBlank(s)) {
//将字符串转为bean
//存在,直接返回
Shop shop = JSONUtil.toBean(s, Shop.class);
return shop;
}
//判断是否店铺是否存在
if (s!=null){
//返回空值
return null;
}
//缓存重建
//尝试获取互斥锁
String lockKey= LOCK_SHOP_KEY+id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
//判断是否获取锁成功
if (!isLock){
//获取锁失败,休眠一会重试
Thread.sleep(10);
return queryWithMutex(id);
}
//获取锁成功,再次查询缓存中是否存在数据,如果存在,则不需要缓存重建
s = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY +id);
//判断是否存在
if (!StrUtil.isBlank(s)) {
//将字符串转为bean
//存在,直接返回
shop = JSONUtil.toBean(s, Shop.class);
unLock(lockKey);
return shop;
}
//判断是否店铺是否存在
if (s!=null){
//返回空值
return null;
}
//缓存中还是没有数据,查询数据库
//不存在,查询数据库
shop = getById(id);
if (shop==null){
//不存在,返回404
//缓存空值到数据库中
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//数据库中是否存在,存在则写入缓存,并返回
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放锁
unLock(lockKey);
}
return shop;
}
逻辑过期解决缓存击穿:
创建一个redisData类,维护不同对象的过期时间:
@Data public class RedisData { private LocalDateTime expireTime; private Object data; }
/**
* 缓存预热将店铺数据存入redis中并且设置逻辑过期时间
*
*/
private void save2Redis(Long id,Long expireSeconds){
//查询店铺信息
Shop shop = getById(id);
//封装RedisData
RedisData redisData=new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//保存数据到redis中
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
private Shop queryWithLogicExpire(Long id){
//根据id在redis中查询数据
String s = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY +id);
//判断是否存在
if (StrUtil.isBlank(s)) {
//缓存未命中不存在,直接返回null
return null;
}
//缓存命中,判断缓存是否过期
RedisData redisData = JSONUtil.toBean(s, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())){
//未过期
return shop;
}
//过期,尝试获取互斥锁
String lockKey= LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
if (isLock){
//TODO 获取互斥锁成功,重新查询缓存是否过期,如果未过期,不用再缓存重建,如果过期,则重建缓存
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
save2Redis(id,20L);
} catch (Exception e) {
e.printStackTrace();
} finally {
unLock(lockKey);
}
});
}
//获取互斥锁失败,返回过期店铺信息
return shop;
}
封装Redis缓存工具类,包含四个方法:
@Component
@Slf4j
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
// 基于构造函数注入,不太使用,不太理解
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//缓存数据到redis中,并设置过期时间
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
//缓存数据到redis中,设置逻辑过期时间
public void setLogicExpire(String key, Object value, Long time, TimeUnit unit){
//设置逻辑过期
RedisData redisData=new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入redis中
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData),time,unit);
}
//缓存穿透解决方案,
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
String key=keyPrefix+id;
//根据id在redis中查询数据
String json = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if (!StrUtil.isBlank(json)) {
//将字符串转为bean
//存在,直接返回
R r= JSONUtil.toBean(json, type);
return r;
}
//判断是否店铺是否存在
if (json!=null){
//返回空值
return null;
}
//不存在,查询数据库
R r = dbFallback.apply(id);
if (r==null){
//不存在,返回404
//缓存空值到数据库中
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//数据库中是否存在,存在则写入缓存,并返回
this.set(key,r,time,unit);
return r;
}
//缓存击穿解决方案:逻辑过期
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
/**
* 使用逻辑过期解决缓存击穿
* @param id
* @return
*/
public <R,ID> R queryWithLogicExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
//根据id在redis中查询数据
String key=keyPrefix+id;
String json = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if (StrUtil.isBlank(json)) {
//缓存未命中不存在,直接返回null
return null;
}
//缓存命中,判断缓存是否过期
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R r=JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())){
//未过期
return r;
}
//过期,尝试获取互斥锁
String lockKey= LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
if (isLock){
//TODO 获取互斥锁成功,重新查询缓存是否过期,如果未过期,不用再缓存重建,如果过期,则重建缓存
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
//查询数据库
R r1= dbFallback.apply(id);
//缓存数据
this.setLogicExpire(key,r1,time,unit);
} catch (Exception e) {
e.printStackTrace();
} finally {
unLock(lockKey);
}
});
}
//获取互斥锁失败,返回过期店铺信息
return r;
}
//使用redis中的setnx实现互斥锁。
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
//潜在的NullReferenceException,如果装箱的对象是null,那么在拆箱时可能会抛出NullReferenceException
return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
}
总结:
Spring中bean的注入方式:
在Spring框架中,注入Bean(对象)的方式有多种,以下是一些常见的方法:
1. 构造器注入(Constructor Injection):
通过构造器注入依赖,可以确保在创建对象时,它所依赖的其他对象也被创建。
```
public class ExampleBean {
private final AnotherBean anotherBean;
@Autowired
public ExampleBean(AnotherBean anotherBean) {
this.anotherBean = anotherBean;
}
}
```
2. Setter方法注入(Setter Injection):
通过setter方法注入依赖,可以在对象创建后,再设置依赖的对象。
```java
public class ExampleBean {
private AnotherBean anotherBean;
@Autowired
public void setAnotherBean(AnotherBean anotherBean) {
this.anotherBean = anotherBean;
}
}
```
3. 字段注入(Field Injection):
通过字段注入依赖,直接在字段上使用`@Autowired`注解。
```java
public class ExampleBean {
@Autowired
private AnotherBean anotherBean;
}
```
4. 方法参数注入(Method Parameter Injection):
在方法参数上使用`@Autowired`注解,Spring会注入对应的依赖。
```java
public class ExampleBean {
public void doSomething(@Autowired AnotherBean anotherBean) {
// ...
}
}
```
5. 接口注入(Interface Injection):
通过定义一个接口来标记需要注入的Bean。
```java
public interface InjectedInterface {
void injectDependency(AnotherBean anotherBean);
}
public class ExampleBean implements InjectedInterface {
private AnotherBean anotherBean;
@Override
public void injectDependency(AnotherBean anotherBean) {
this.anotherBean = anotherBean;
}
}
```
6. 基于注解的注入(Annotation-based Injection):
使用`@Autowired`、`@Resource`、`@Inject`等注解来标记需要注入的依赖。
```java
public class ExampleBean {
@Autowired
private AnotherBean anotherBean;
}
```
7. 自动装配(Autowiring):
Spring可以自动装配依赖,无需显式注入。
```java
public class ExampleBean {
@Autowired
private AnotherBean anotherBean;
}
```
8. 基于Java配置的注入:
使用`@Configuration`和`@Bean`注解来定义和注入Bean。
```java
@Configuration
public class AppConfig {
@Bean
public ExampleBean exampleBean() {
return new ExampleBean(anotherBean());
}
@Bean
public AnotherBean anotherBean() {
return new AnotherBean();
}
}
```
选择哪种注入方式取决于你的具体需求和设计偏好。构造器注入和setter注入是最常用的方式,因为它们可以保证依赖的完整性和初始化的一致性。而字段注入和基于注解的注