目录
引出
Redis的应用------接口幂等性,分布式锁,基于注解+拦截器的接口幂等改进
接口幂等&分布式锁
系统的问题:
接口的幂等性:接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;
java
@RestController
@RequestMapping("/order")
public class OrderController {
private final RedisTemplate redisTemplate;
public OrderController(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@PostMapping("/make")
public String makeOrder(OrderDto orderDto){
if(redisTemplate.hasKey("ORDER_" + orderDto.getOrderNum())){
return "请求已经处理,请勿重复提交!";
}
//setnx key value 做到:判断key是否存在,如果key则不能加入到redis中去,称为:redis的分布式锁
redisTemplate.opsForValue().setIfAbsent("ORDER_" + orderDto.getOrderNum(), "",10, TimeUnit.MINUTES);
//调用业务层:完成订单的创建
System.out.println("orderDto = " + orderDto);
return "ok";
}
}
接口幂等性
java
if(redisTemplate.hasKey("ORDER_" + orderDto.getOrderNum())){
return "请求已经处理,请勿重复提交!";
}
redis的分布锁,缺陷:业务如果超出了上锁的时间,可能会导致数据不准确
Redisson框架
Redisson框架:它是Redis的封装框架,提供了Redis的所有操作
xml
<!--引入redis的封装框架redission-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.0</version>
</dependency>
添加配置类:
java
@Configuration
public class RedissonConfig {
@Autowired
private Environment env;
//解决redission 和 Springcache的兼容问题
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 设置缓存数据的key序列化方式为StringRedisSerializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // 设置缓存数据的value序列化方式为GenericJackson2JsonRedisSerializer
RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
return cacheManager;
}
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
SingleServerConfig serverConfig = config.useSingleServer();
// 设置 Redis 单节点地址和密码
String address = "redis://" + env.getProperty("spring.redis.host") + ":" + env.getProperty("spring.redis.port");
serverConfig.setAddress(address);
serverConfig.setPassword(env.getProperty("spring.redis.password"));
return Redisson.create(config);
}
}
Redis上锁的方式:
java
//第1种
redisTemplate.opsForValue.setIfAbsent(key,value,time,TimeUnit);
//第2种
RLock lock = redissonClient.getLock(key);
lock.lock(time,TimeUnit)//上锁
不管是第1种,还是第2种,底层都是:setnx key value px time;上面的2种有可能有问题,问题在于:业务可能超过:上锁的时间,导致业务还没完成,锁就已经失效
解决方案:看门狗机制
java
RLock lock = redissonClient.getLock(key);
try {
//获得一把锁,锁的时间默认是30S,间隔10S检测一次锁的状态,如果超过10S锁的状态,仍旧是在使用,就把锁的时间重置为30S
//配置 看门狗
lock = redissonClient.getLock(lock.getName());
lock.lock();//上锁
//写:上锁的以后的业务代码
}catch (Exception e){
}finally {
lock.unlock();//立即解锁
}
接口幂等改进
使用 注解 + 拦截器完成对于 接口 进行幂等处理
HTML页面
OrderController
添加接口幂等前缀常量:
java
public class BookConstant {
public static final String BOOK_TYPE_LIST = "BOOKTYPELIST";
public static final String TOKEN_PREFIX= "TOKEN_";
public static final String USER_CART_PREFIX = "USER_CART_";
//接口幂等
public static final String INTERFACE_IDEA_PREFIX="INTERFACE_IDEM_";
}
自定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 幂等过期时间,即:在此时间段内,对API进行幂等处理。
*/
long expireTime();
/**
* 时间单位
* @return
*/
TimeUnit timeunit();
}
自定义拦截器
java
public class IdempotentInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
/**
* 控制层方法,执行之前进行拦截
* @param request 请求对象
* @param response 响应对象
* @param handler 方法对象
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("handler = " + handler.getClass());
//不是拦截方法,则放行
if(!(handler instanceof HandlerMethod))return true;
//判断方法对象是否添加 幂等注解
HandlerMethod hm = (HandlerMethod) handler;
boolean flag = hm.hasMethodAnnotation(Idempotent.class);
if(!flag) return true;//如果没有使用幂等注解,直接放行
//如果使用幂等注解,启动幂等配置
Idempotent idem = hm.getMethodAnnotation(Idempotent.class);
//identification = 前端:请求的唯一标识
String identification = request.getHeader("Identification");
System.out.println("Identification = " + identification);
System.out.println("idem.expireTime() = " + idem.expireTime());
System.out.println("idem.timeunit() = " + idem.timeunit());
Boolean b = redisTemplate.hasKey(identification);
if(b){
extracted(response);
return false;
}else{
redisTemplate.opsForValue().setIfAbsent(identification,"",idem.expireTime(),idem.timeunit());
}
return true;
}
private static void extracted(HttpServletResponse response) throws IOException {
//告诉前端:请求是重复请求,请勿重复提交!
ResponseResult res = ResponseResult.fail(BusinessEnum.HTTP_ALREADY_HANDLE);
String s = JSONUtil.toJsonStr(res);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(s);
}
}
配置拦截器
java
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
//......
@Bean
public IdempotentInterceptor idempotentInterceptor(){
return new IdempotentInterceptor();
}
//......
@Override
public void addInterceptors(InterceptorRegistry registry) {
//......
//向SpringMVC中 注册 幂等拦截器
registry.addInterceptor(idempotentInterceptor()).addPathPatterns("/order/sumbitOrder/**");
}
//......
}
前端axios提交时,添加identification请求头:
java
submitOrder(){
axios.post('/order/sumbitOrder/'+this.addressId+"/"+this.orderNum,this.cartVo,{
headers: {
'identification': this.identification, // 设置请求头的Content-Type为application/json
}
}).then(res=>{
if(res.data.code == 200){
this.$message.success("订单提交成功!");
setTimeout(()=>{
location.href = "/order.html";
},3000)
}else{
this.$message.error(res.data.msg);
}
});
}
控制器上:添加@Idempotent 幂等注解
java
@Idempotent(expireTime = 30,timeunit = TimeUnit.SECONDS)
@PostMapping("/sumbitOrder/{addressId}/{orderNum}")
public ResponseResult sumbitOrder(@RequestBody CartVo cartVo,
@PathVariable("addressId") Integer addressId,
@PathVariable("orderNum") String orderNum,
@RequestHeader("Authorization") String token){
//......
}
缓存三兄弟:缓存击穿、穿透、雪崩
缓存击穿
缓存击穿:redis中没有,但是数据库有
顺序:先查缓存,判断缓存是否存在;如果缓存存在,直接返回数据;如果缓存不存在,則查询数据库,将数据库的数据存入到缓存
解决方案:将热点数据设置过期时间长一点;针对数据库的热点访问方法上分布式锁;
缓存穿透
缓存穿透:redis中没有,数据库也没有
解决方案:
(1)将不存在的key,在redis设置值为null;
(2)使用布隆过滤器;
原理:https://zhuanlan.zhihu.com/p/616911933
布隆过滤器:
如果确认key不存在于redis中,那么就一定不存在;
它说key存在,就有可能存在,也可能不存在! (误差)
布隆过滤器
1、根据配置类中的 key的数量 ,误差率,计算位图数组【二维数组】
2、通过布隆过滤器存放key的时候,会计算出需要多少个hash函数,由hash函数算出多少个位图位置需要设定为1
3、查询时,根据对应的hash函数,判断对应的位置值是否都为1;如果有位置为0,则表示key一定不存在于该redis服务器中;如果全部位置都为1,则表示key可能存在于redis服务器中;
缓存雪崩
缓存雪崩:
Redis的缓存雪崩是指当Redis中大量缓存数据同时失效或者被清空时,大量的请求会直接打到数据库上,导致数据库瞬时压力过大,甚至宕机的情况。
造成缓存雪崩的原因主要有两个:
1.相同的过期时间:当Redis中大量的缓存数据设置相同的过期时间时,这些数据很可能会在同一时间点同时失效,导致大量请求直接打到数据库上。
2.缓存集中失效:当服务器重启、网络故障等因素导致Redis服务不可用,且缓存数据没有自动进行容错处理,当服务恢复时大量的数据同时被重新加载到缓存中,也会导致大量请求直接打到数据库上。
预防缓存雪崩的方法主要有以下几种:
1.设置不同的过期时间:可以将缓存数据的过期时间分散开,避免大量缓存数据在同一时间点失效。
2.使用加锁:可以将所有请求都先进行加锁操作,当某个请求去查询数据库时,如果还没有加载到缓存中,则只让单个线程去执行加载操作,其他线程等待该线程完成后再次进行判断,避免瞬间都去访问数据库从而引起雪崩。
3.提前加载预热:在系统低峰期,可以提前将部分热点数据加载到缓存中,这样可以避免在高峰期缓存数据失效时全部打到数据库上。
4.使用多级缓存:可以在Redis缓存之上再使用一层缓存,例如本地缓存等,当Redis缓存失效时,还能够从本地缓存中获取数据,避免直接打到数据库上。
本地缓存:ehcache oscache spring自带缓存 持久层框架的缓存
总结
Redis的应用------接口幂等性,分布式锁,基于注解+拦截器的接口幂等改进