SpringCloud微服务实战——搭建企业级开发框架(三十九):使用Redis分布式锁(Redisson)+自定义注解+AOP实现微服务重复请求控制

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
注册配置 MSE Nacos/ZooKeeper,118元/月
云原生网关 MSE Higress,422元/月
简介: 通常我们可以在前端通过防抖和节流来解决短时间内请求重复提交的问题,如果因网络问题、Nginx重试机制、微服务Feign重试机制或者用户故意绕过前端防抖和节流设置,直接频繁发起请求,都会导致系统防重请求失败,甚至导致后台产生多条重复记录,此时我们需要考虑在后台增加防重设置。

通常我们可以在前端通过防抖和节流来解决短时间内请求重复提交的问题,如果因网络问题、Nginx重试机制、微服务Feign重试机制或者用户故意绕过前端防抖和节流设置,直接频繁发起请求,都会导致系统防重请求失败,甚至导致后台产生多条重复记录,此时我们需要考虑在后台增加防重设置。


  考虑到微服务分布式的场景,这里通过使用Redisson分布式锁+自定义注解+AOP的方式来实现后台防止重复请求的功能,基本实现思路:通过在需要防重的接口添加自定义防重注解,设置防重参数,通过AOP拦截请求参数,根据注解配置,生成分布式锁的Key,并设置有效时间。每次请求访问时,都会尝试获取锁,如果获取到,则执行,如果获取不到,那么说明请求在设置的重复请求间隔内,返回请勿频繁请求提示信息。


1、自定义防止重复请求注解,根据业务场景设置了以下参数:


  • interval: 防止重复提交的时间间隔。


  • timeUnit: 防止重复提交的时间间隔的单位。


  • currentSession: 是否将sessionId作为防重参数(微服务及跨域前后端分离时,无法使用,Chrome等浏览器跨域时禁止携带cookie,每次sessionId都是新的)。


  • currentUser: 是否将用户id作为防重参数。


  • keys: 可以作为防重参数的字段(通过Spring Expression表达式,可以做到多参数时,具体取哪个参数的值)。


  • ignoreKeys: 需要忽略的防重参数字段,例如有些参数中的时间戳,此和keys互斥,当keys配置了之后,ignoreKeys失效。


  • conditions:当参数中的某个字段达到条件时,执行防重配置,默认不需要配置。


  • argsIndex: 当没有配置keys参数时,防重拦截后会对所有参数取值作为分布式锁的key,这里时,当多参数时,配置取哪一个参数作为key,可以多个。此和keys互斥,当keys配置了之后,argsIndex配置失效。


package com.gitegg.platform.base.annotation.resubmit;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
 * 防止重复提交注解
 * 1、当设置了keys时,通过表达式确定取哪几个参数作为防重key
 * 2、当未设置keys时,可以设置argsIndex设置取哪几个参数作为防重key
 * 3、argsIndex和ignoreKeys是未设置keys时生效,排除不需要防重的参数
 * 4、因部分浏览器在跨域请求时,不允许request请求携带cookie,导致每次sessionId都是新的,所以这里默认使用用户id作为key的一部分,不使用sessionId
 * @author GitEgg
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResubmitLock {
    /**
     * 防重复提交校验的时间间隔
     */
    long interval() default 5;
    /**
     * 防重复提交校验的时间间隔的单位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     * 是否仅在当前session内进行防重复提交校验
     */
    boolean currentSession() default false;
    /**
     * 是否选用当前操作用户的信息作为防重复提交校验key的一部分
     */
    boolean currentUser() default true;
    /**
     * keys和ignoreKeys不能同时使用
     * 参数Spring EL表达式例如 #{param.name},表达式的值作为防重复校验key的一部分
     */
    String[] keys() default {};
    /**
     * keys和ignoreKeys不能同时使用
     * ignoreKeys不区分入参,所有入参拥有相同的字段时,都将过滤掉
     */
    String[] ignoreKeys() default {};
    /**
     * Spring EL表达式,决定是否进行重复提交校验,多个条件之间为且的关系,默认是进行校验
     */
    String[] conditions() default {"true"};
    /**
     * 当未配置key时,设置哪几个参数作为防重对象,默认取所有参数
     *
     * @return
     */
    int[] argsIndex() default {};
}


2、自定义AOP拦截防重请求的业务逻辑处理,详细逻辑处理请看代码注释。可以在Nacos中增加配置resubmit-lock: enable: false 使防重配置失效,默认不配置为生效状态。因为是ResubmitLockAspect是否初始化的ConditionalOnProperty配置,此配置修改需要重启服务生效。


package com.gitegg.platform.boot.aspect;
import com.gitegg.platform.base.annotation.resubmit.ResubmitLock;
import com.gitegg.platform.base.enums.ResultCodeEnum;
import com.gitegg.platform.base.exception.SystemException;
import com.gitegg.platform.base.util.JsonUtils;
import com.gitegg.platform.boot.util.ExpressionUtils;
import com.gitegg.platform.boot.util.GitEggAuthUtils;
import com.gitegg.platform.boot.util.GitEggWebUtils;
import com.gitegg.platform.redis.lock.IDistributedLockService;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
/**
 * @author GitEgg
 * @date 2022-4-10
 */
@Log4j2
@Component
@Aspect
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@ConditionalOnProperty(name = "enabled", prefix = "resubmit-lock", havingValue = "true", matchIfMissing = true)
public class ResubmitLockAspect {
    private static final String REDIS_SEPARATOR = ":";
    private static final String RESUBMIT_CHECK_KEY_PREFIX = "resubmit_lock" + REDIS_SEPARATOR;
    private final IDistributedLockService distributedLockService;
    /**
     * Before切点
     */
    @Pointcut("@annotation(com.gitegg.platform.base.annotation.resubmit.ResubmitLock)")
    public void resubmitLock() {
    }
    /**
     * 前置通知 防止重复提交
     *
     * @param joinPoint 切点
     * @param resubmitLock 注解配置
     */
    @Before("@annotation(resubmitLock)")
    public Object resubmitCheck(JoinPoint joinPoint, ResubmitLock resubmitLock) throws Throwable {
        final Object[] args = joinPoint.getArgs();
        final String[] conditions = resubmitLock.conditions();
        //根据条件判断是否需要进行防重复提交检查
        if (!ExpressionUtils.getConditionValue(args, conditions) || ArrayUtils.isEmpty(args)) {
            return ((ProceedingJoinPoint) joinPoint).proceed();
        }
        doCheck(resubmitLock, args);
        return ((ProceedingJoinPoint) joinPoint).proceed();
    }
    /**
     * key的组成为: resubmit_lock:userId:sessionId:uri:method:(根据spring EL表达式对参数进行拼接)
     *
     * @param resubmitLock 注解
     * @param args       方法入参
     */
    private void doCheck(@NonNull ResubmitLock resubmitLock, Object[] args) {
        final String[] keys = resubmitLock.keys();
        final boolean currentUser = resubmitLock.currentUser();
        final boolean currentSession = resubmitLock.currentSession();
        String method = GitEggWebUtils.getRequest().getMethod();
        String uri = GitEggWebUtils.getRequest().getRequestURI();
        StringBuffer lockKeyBuffer = new StringBuffer(RESUBMIT_CHECK_KEY_PREFIX);
        if (null != GitEggAuthUtils.getTenantId())
        {
            lockKeyBuffer.append( GitEggAuthUtils.getTenantId()).append(REDIS_SEPARATOR);
        }
        // 此判断暂时预留,适配后续无用户登录场景,因部分浏览器在跨域请求时,不允许request请求携带cookie,导致每次sessionId都是新的,所以这里默认使用用户id作为key的一部分,不使用sessionId
        if (currentSession)
        {
            lockKeyBuffer.append( GitEggWebUtils.getSessionId()).append(REDIS_SEPARATOR);
        }
        // 默认没有将user数据作为防重key
        if (currentUser && null != GitEggAuthUtils.getCurrentUser())
        {
            lockKeyBuffer.append( GitEggAuthUtils.getCurrentUser().getId() ).append(REDIS_SEPARATOR);
        }
        lockKeyBuffer.append(uri).append(REDIS_SEPARATOR).append(method);
        StringBuffer parametersBuffer = new StringBuffer();
        // 优先判断是否设置防重字段,因keys试数组,取值时是按照顺序排列的,这里不需要重新排序
        if (ArrayUtils.isNotEmpty(keys))
        {
            Object[] argsForKey = ExpressionUtils.getExpressionValue(args, keys);
            for (Object obj : argsForKey) {
                parametersBuffer.append(REDIS_SEPARATOR).append(String.valueOf(obj));
            }
        }
        // 如果没有设置防重的字段,那么需要把所有的字段和值作为key,因通过反射获取字段时,顺序时不确定的,这里取出来之后需要进行排序
        else{
            // 只有当keys为空时,ignoreKeys和argsIndex生效
            final String[] ignoreKeys = resubmitLock.ignoreKeys();
            final int[] argsIndex = resubmitLock.argsIndex();
            if (ArrayUtils.isNotEmpty(argsIndex))
            {
                for(int index : argsIndex){
                    parametersBuffer.append(REDIS_SEPARATOR).append( getKeyAndValueJsonStr(args[index], ignoreKeys));
                }
            }
            else
            {
                for(Object obj : args){
                    parametersBuffer.append(REDIS_SEPARATOR).append( getKeyAndValueJsonStr(obj, ignoreKeys) );
                }
            }
        }
        // 将请求参数取md5值作为key的一部分,MD5理论上会重复,但是key中还包含session或者用户id,所以同用户在极端时间内请参数不同生成的相同md5值的概率极低
        String parametersKey = DigestUtils.md5DigestAsHex(parametersBuffer.toString().getBytes());
        lockKeyBuffer.append(parametersKey);
        try {
            boolean isLock = distributedLockService.tryLock(lockKeyBuffer.toString(), 0, resubmitLock.interval(), resubmitLock.timeUnit());
            if (!isLock)
            {
                throw new SystemException(ResultCodeEnum.RESUBMIT_LOCK.code, ResultCodeEnum.RESUBMIT_LOCK.msg);
            }
        } catch (InterruptedException e) {
            throw new SystemException(ResultCodeEnum.RESUBMIT_LOCK.code, ResultCodeEnum.RESUBMIT_LOCK.msg);
        }
    }
    /**
     * 将字段转换为json字符串
     * @param obj
     * @return
     */
    public static String getKeyAndValueJsonStr(Object obj, String[] ignoreKeys) {
        Map<String, Object> map = Maps.newHashMap();
        // 得到类对象
        Class objCla = (Class) obj.getClass();
        /* 得到类中的所有属性集合 */
        Field[] fs = objCla.getDeclaredFields();
        for (int i = 0; i < fs.length; i++) {
            Field f = fs[i];
            // 设置些属性是可以访问的
            f.setAccessible(true);
            Object val = new Object();
            try {
                String filedName = f.getName();
                // 如果字段在排除列表,那么不将字段放入map
                if (null != ignoreKeys && Arrays.asList(ignoreKeys).contains(filedName))
                {
                    continue;
                }
                val = f.get(obj);
                // 得到此属性的值
                // 设置键值
                map.put(filedName, val);
            } catch (IllegalArgumentException e) {
                log.error("getKeyAndValue IllegalArgumentException", e);
                throw new RuntimeException("您的操作太频繁,请稍后再试");
            } catch (IllegalAccessException e) {
                log.error("getKeyAndValue IllegalAccessException", e);
                throw new RuntimeException("您的操作太频繁,请稍后再试");
            }
        }
        Map<String, Object> sortMap = sortMapByKey(map);
        String mapStr = JsonUtils.mapToJson(sortMap);
        return mapStr;
    }
    private static Map<String, Object> sortMapByKey(Map<String, Object> map) {
        if (map == null || map.isEmpty()) {
            return null;
        }
        Map<String, Object> sortMap = new TreeMap<String, Object>(new Comparator<String>() {
            @Override
            public int compare(String o1,String o2) {
                return ((String)o1).compareTo((String) o2);
            }
        });
        sortMap.putAll(map);
        return sortMap;
    }
}


3、Redisson分布式锁自定义接口


package com.gitegg.platform.redis.lock;
import java.util.concurrent.TimeUnit;
/**
 * 分布式锁接口
 * @author GitEgg
 * @date 2022-4-10
 */
public interface IDistributedLockService {
    /**
     * 加锁
     * @param lockKey key
     */
    void lock(String lockKey);
    /**
     * 释放锁
     *
     * @param lockKey key
     */
    void unlock(String lockKey);
    /**
     * 加锁并设置有效期
     *
     * @param lockKey key
     * @param timeout 有效时间,默认时间单位在实现类传入
     */
    void lock(String lockKey, int timeout);
    /**
     * 加锁并设置有效期指定时间单位
     * @param lockKey key
     * @param timeout 有效时间
     * @param unit    时间单位
     */
    void lock(String lockKey, int timeout, TimeUnit unit);
    /**
     * 尝试获取锁,获取到则持有该锁返回true,未获取到立即返回false
     * @param lockKey
     * @return true-获取锁成功 false-获取锁失败
     */
    boolean tryLock(String lockKey);
    /**
     * 尝试获取锁,获取到则持有该锁leaseTime时间.
     * 若未获取到,在waitTime时间内一直尝试获取,超过watiTime还未获取到则返回false
     * @param lockKey   key
     * @param waitTime  尝试获取时间
     * @param leaseTime 锁持有时间
     * @param unit      时间单位
     * @return true-获取锁成功 false-获取锁失败
     * @throws InterruptedException
     */
    boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
            throws InterruptedException;
    /**
     * 锁是否被任意一个线程锁持有
     * @param lockKey
     * @return true-被锁 false-未被锁
     */
    boolean isLocked(String lockKey);
}


4、Redisson分布式锁自定义接口实现类


package com.gitegg.platform.redis.lock.impl;
import com.gitegg.platform.redis.lock.IDistributedLockService;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
 * 分布式锁的 Redisson 接口实现
 * @author GitEgg
 * @date 2022-4-10
 */
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class DistributedLockServiceImpl implements IDistributedLockService {
    private final RedissonClient redissonClient;
    @Override
    public void lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
    }
    @Override
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }
    @Override
    public void lock(String lockKey, int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, TimeUnit.MILLISECONDS);
    }
    @Override
    public void lock(String lockKey, int timeout, TimeUnit unit) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
    }
    @Override
    public boolean tryLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock();
    }
    @Override
    public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock(waitTime, leaseTime, unit);
    }
    @Override
    public boolean isLocked(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.isLocked();
    }
}


5、Spring Expression自定义工具类,通过此工具类获取注解上的Expression表达式,以获取相应请求对象的值,如果请求对象有多个,可以通过Expression表达式精准获取。


package com.gitegg.platform.boot.util;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * Spring Expression 工具类
 * @author GitEgg
 * @date 2022-4-11
 */
public class ExpressionUtils {
    private static final Map<String, Expression> EXPRESSION_CACHE = new ConcurrentHashMap<>(64);
    /**
     * 获取Expression对象
     *
     * @param expressionString Spring EL 表达式字符串 例如 #{param.id}
     * @return Expression
     */
    @Nullable
    public static Expression getExpression(@Nullable String expressionString) {
        if (StringUtils.isBlank(expressionString)) {
            return null;
        }
        if (EXPRESSION_CACHE.containsKey(expressionString)) {
            return EXPRESSION_CACHE.get(expressionString);
        }
        Expression expression = new SpelExpressionParser().parseExpression(expressionString);
        EXPRESSION_CACHE.put(expressionString, expression);
        return expression;
    }
    /**
     * 根据Spring EL表达式字符串从根对象中求值
     *
     * @param root             根对象
     * @param expressionString Spring EL表达式
     * @param clazz            值得类型
     * @param <T>              泛型
     * @return 值
     */
    @Nullable
    public static <T> T getExpressionValue(@Nullable Object root, @Nullable String expressionString, @NonNull Class<? extends T> clazz) {
        if (root == null) {
            return null;
        }
        Expression expression = getExpression(expressionString);
        if (expression == null) {
            return null;
        }
        return expression.getValue(root, clazz);
    }
    @Nullable
    public static <T> T getExpressionValue(@Nullable Object root, @Nullable String expressionString) {
        if (root == null) {
            return null;
        }
        Expression expression = getExpression(expressionString);
        if (expression == null) {
            return null;
        }
        //noinspection unchecked
        return (T) expression.getValue(root);
    }
    /**
     * 求值
     *
     * @param root              根对象
     * @param expressionStrings Spring EL表达式
     * @param <T>               泛型 这里的泛型要慎用,大多数情况下要使用Object接收避免出现转换异常
     * @return 结果集
     */
    public static <T> T[] getExpressionValue(@Nullable Object root, @Nullable String... expressionStrings) {
        if (root == null) {
            return null;
        }
        if (ArrayUtils.isEmpty(expressionStrings)) {
            return null;
        }
        //noinspection ConstantConditions
        Object[] values = new Object[expressionStrings.length];
        for (int i = 0; i < expressionStrings.length; i++) {
            //noinspection unchecked
            values[i] = (T) getExpressionValue(root, expressionStrings[i]);
        }
        //noinspection unchecked
        return (T[]) values;
    }
    /**
     * 表达式条件求值
     * 如果为值为null则返回false,
     * 如果为布尔类型直接返回,
     * 如果为数字类型则判断是否大于0
     *
     * @param root             根对象
     * @param expressionString Spring EL表达式
     * @return 值
     */
    @Nullable
    public static boolean getConditionValue(@Nullable Object root, @Nullable String expressionString) {
        Object value = getExpressionValue(root, expressionString);
        if (value == null) {
            return false;
        }
        if (value instanceof Boolean) {
            return (boolean) value;
        }
        if (value instanceof Number) {
            return ((Number) value).longValue() > 0;
        }
        return true;
    }
    /**
     * 表达式条件求值
     *
     * @param root              根对象
     * @param expressionStrings Spring EL表达式数组
     * @return 值
     */
    @Nullable
    public static boolean getConditionValue(@Nullable Object root, @Nullable String... expressionStrings) {
        if (root == null) {
            return false;
        }
        if (ArrayUtils.isEmpty(expressionStrings)) {
            return false;
        }
        //noinspection ConstantConditions
        for (String expressionString : expressionStrings) {
            if (!getConditionValue(root, expressionString)) {
                return false;
            }
        }
        return true;
    }
}


5、防重测试,我们在系统的用户接口(GitEgg-Cloud工程的UserController类)上进行测试,通过多参数接口以及配置keys,不配置keys等各种场景进行测试,在测试时为了达到效果,可以将interval 时间设置为30秒。


  • 设置user参数的realName,mobile和page参数的size为key进行防重测试


@ResubmitLock(interval = 30, keys = {"[0].realName","[0].mobile","[1].size"})
    public PageResult<UserInfo> list(@ApiIgnore QueryUserDTO user, @ApiIgnore Page<UserInfo> page) {
        Page<UserInfo> pageUser = userService.selectUserList(page, user);
        PageResult<UserInfo> pageResult = new PageResult<>(pageUser.getTotal(), pageUser.getRecords());
        return pageResult;
    }


  • 不设置防重参数的key,只取第一个参数user,配置排除的参数,不参与放重key的生成


@ResubmitLock(interval = 30, argsIndex = {0}, ignoreKeys = {"email","status"})
    public PageResult<UserInfo> list(@ApiIgnore QueryUserDTO user, @ApiIgnore Page<UserInfo> page) {
        Page<UserInfo> pageUser = userService.selectUserList(page, user);
        PageResult<UserInfo> pageResult = new PageResult<>(pageUser.getTotal(), pageUser.getRecords());
        return pageResult;
    }


  • 测试结果
    微信图片_20220519225959.png

    测试结果


相关引用:


1、防重配置项及通过SpringExpression获取相应参数:https://www.jianshu.com/p/77895a822237


2、Redisson分布式锁及相关工具类:https://blog.csdn.net/wsh_ningjing/article/details/115326052

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
Java API 数据安全/隐私保护
(工作经验)优雅实现接口权限校验控制:基于自定义注解、AOP与@ConditionalOnProperty配置开关的通用解决方案
(工作经验)优雅实现接口权限校验控制:基于自定义注解、AOP与@ConditionalOnProperty配置开关的通用解决方案
81 1
|
1月前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
48 1
|
2月前
|
NoSQL Java Redis
redis的基本命令,并用netty操作redis(不使用springboot或者spring框架)就单纯的用netty搞。
这篇文章介绍了Redis的基本命令,并展示了如何使用Netty框架直接与Redis服务器进行通信,包括设置Netty客户端、编写处理程序以及初始化Channel的完整示例代码。
67 1
redis的基本命令,并用netty操作redis(不使用springboot或者spring框架)就单纯的用netty搞。
|
2月前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
39 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
2月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
83 2
|
2月前
|
NoSQL API Redis
如何使用 C++ 开发 Redis 模块
如何使用 C++ 开发 Redis 模块
|
3月前
|
消息中间件 NoSQL Go
PHP转Go系列 | ThinkPHP与Gin框架之Redis延时消息队列技术实践
【9月更文挑战第7天】在从 PHP 的 ThinkPHP 框架迁移到 Go 的 Gin 框架时,涉及 Redis 延时消息队列的技术实践主要包括:理解延时消息队列概念,其能在特定时间处理消息,适用于定时任务等场景;在 ThinkPHP 中使用 Redis 实现延时队列;在 Gin 中结合 Go 的 Redis 客户端库实现类似功能;Go 具有更高性能和简洁性,适合处理大量消息。迁移过程中需考虑业务需求及系统稳定性。
|
4月前
|
负载均衡 Java API
深度解析SpringCloud微服务跨域联动:RestTemplate如何驾驭HTTP请求,打造无缝远程通信桥梁
【8月更文挑战第3天】踏入Spring Cloud的微服务世界,服务间的通信至关重要。RestTemplate作为Spring框架的同步客户端工具,以其简便性成为HTTP通信的首选。本文将介绍如何在Spring Cloud环境中运用RestTemplate实现跨服务调用,从配置到实战代码,再到注意事项如错误处理、服务发现与负载均衡策略,帮助你构建高效稳定的微服务系统。
105 2
|
5月前
|
Java 微服务 Spring
SpringCloud gateway自定义请求的 httpClient
SpringCloud gateway自定义请求的 httpClient
227 3
|
4月前
|
存储 缓存 NoSQL
【Azure Redis 缓存】由Azure Redis是否可以自定义密码而引申出Azure PaaS的Redis服务是否可以和自建的Redis进行主从配置呢?
【Azure Redis 缓存】由Azure Redis是否可以自定义密码而引申出Azure PaaS的Redis服务是否可以和自建的Redis进行主从配置呢?
下一篇
DataWorks