Redis实现计数器功能(接口最大访问次数检查)
1.前言
备注:不要全部粘贴代码,这个是我练习项目的代码(基于spring-boot),主要看2和3的逻辑
1.还是一个简单的redis使用Demo,包含setIfAbsent(不存在则插入),setIfPresent(存在则更新),getExpire(获取剩余有效时间)
2.大体业务逻辑如下:检查用户请求系统的最大次数,每日最大请求次数为10.
3.也可以使用定时任务实现时效性配置(定时任务00:00执行,清空所有用户访问次数)
2.Redis配置
pom文件redis配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
properties文件redis配置:
#redis 配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.jedis.pool.max-active=8
spring.redis.timeout=2000
redis序列化配置:
配置序列化,防止插入redis乱码(默认二进制)
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisUtil {
/**
* 因为序列化使用的jdkSerializeable ,redis存储数据默认为二进制,简单来说key和value是乱码
* 所以自定义序列化类
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
3.redis判断访问次数
RedisService代码:
public interface RedisService {
/**
* 更新时效性数据--String类型
**/
Boolean updateVaildValue(String key, String value);
/**
* 判断当前用户是否达到访问最大次数
**/
Boolean judgeMaxTimesByUserId(String key, String min, String max);
}
RedisServiceImpl代码:
import com.example.demo.util.redisUtil.RedisService.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* @Author longtao
* @Date 2020/10/20
* @Describe 更新失效性数据 保留剩余有效时间
**/
@Override
public synchronized Boolean updateVaildValue(String key, String value) {
//获取剩余时间 单位秒
Long balanceSeconds = stringRedisTemplate.opsForValue().getOperations().getExpire(key);
if (balanceSeconds > 0) {
return stringRedisTemplate.opsForValue().setIfPresent(key, value, balanceSeconds, TimeUnit.SECONDS);
}
return false;
}
/**
* @Author longtao
* @Date 2020/10/20
* @Describe 判断 最大访问次数 > userId当前访问次数
**/
@Override
public synchronized Boolean judgeMaxTimesByUserId(String key, String min, String max) {
//获取key的值,为null时则插入新的
//不为null时,取出数据进行判断:是否达到最大值
String value = stringRedisTemplate.opsForValue().get(key);
if(StringUtils.isEmpty(value)){
//这里时间暂用的24小时,也可以计算得到当前时间到24:00点的毫秒时间。
stringRedisTemplate.opsForValue().setIfAbsent(key,min,24,TimeUnit.HOURS);
return true;
}
//最大次数 <= 当前访问次数
if(new BigDecimal(max).compareTo(new BigDecimal(value)) <= 0 ){
return false;
}
return updateVaildValue(key,new BigDecimal(value).add(new BigDecimal(1)).toString());
}
}
4.切面调用检查逻辑
切面注解----CheckVisitTimesAroundAnnotation代码:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckVisitTimesAroundAnnotation {
}
切面注解----CheckVisitTimesAroundAspection代码:
//返回异常对象
return new BaseResponse<>(ResultEnum.CHECK_USER_ID_VISIT_TIMES);
这个可以换成日志输出,这里直接粘贴了我的Demo项目代码。
import com.alibaba.excel.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.base.Enum.ResultEnum;
import com.example.demo.base.model.baseModel.BaseModel;
import com.example.demo.base.model.baseRequest.BaseRequest;
import com.example.demo.base.model.baseResponse.BaseResponse;
import com.example.demo.util.redisUtil.RedisService.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
@Slf4j
@Order(2)
public class CheckVisitTimesAroundAspection {
@Autowired
private RedisService redisService;
/**
* 切入点
*/
@Pointcut("@annotation(com.example.demo.base.annonation.CheckVisitTimesAroundAnnotation) ")
public void entryPoint() {
}
/**
* @return
* @Describe 前置切面
*/
@Around("entryPoint()")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
log.info("------检查userId访问次数限制------start");
Object[] args = proceedingJoinPoint.getArgs();
//打印入口方法以及入参
try {
for (Object arg : args) {
Boolean flag = judgeVisitTimesByUserId(arg);
if(!flag){
//返回异常对象
return new BaseResponse<>(ResultEnum.CHECK_USER_ID_VISIT_TIMES);
}
}
} catch (Exception e) {
e.printStackTrace();
log.debug("不能打印请求参数");
}
//执行请求方法
Object o = null;
try {
o = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
log.info("------检查userId访问次数限制------end");
return o;
}
//检查userId请求接口限制
public Boolean judgeVisitTimesByUserId(Object object){
BaseModel baseModel = JSONObject.parseObject(JSONObject.toJSONString(object),BaseModel.class);
String userId = baseModel.getUserId();
if(!StringUtils.isEmpty(userId)){
return redisService.judgeMaxTimesByUserId(userId,"1","10");
}
return true;
}
}
在controller层方法上增加注解代码:
增加@CheckVisitTimesAroundAnnotation注解
@BaseBeforeAnnotation
@CheckVisitTimesAroundAnnotation
@RequestMapping("selectOne")
public BaseResponse selectReaderInfo(@RequestBody ReaderInfoModel model) {
ReaderInfoModel bookInfoModel = readerInfoService.selectOne(model.getId());
return new BaseResponse(ResultEnum.SUCCESS,bookInfoModel);
}
4.执行结果
redis中:userId 的值:
postman收到的返回值:
最后:本次写的比较仓促,我要接我老婆了。有不足的地方望大家指正和包涵!