基于Redis实现分布式锁、限流操作——基于SpringBoot实现
本文总结了一种利用Redis实现分布式锁、限流的较优雅的实现方式 本文原理介绍较为通俗,希望能帮到有需要的人 本文的demo地址:https://gitee.com/rederxu/lock_distributed.git
一、本文基本实现
利用redis的key是否存在判断锁是否存在 利用redis的increment/decrement方法进行计数,从而实现限流 利用注解,对需要锁定/限流的方法进行配置(用起来超简单!!!) 利用SpringBoot的拦截器,在访问前判断并加锁,访问完成后释放锁 利用@ControllerAdvice捕获全局异常和终止访问
二、为什么选redis
由于redis的原子性(即其操作属于最基本的操作,要么执行要么不执行,要么全部成功,要么全部不成功),因此,用来计数、记录值是否存在具有较高优势。 此外,redis作为效率极高的程序外应用,能有效地独立保存数据,即使是多个服务,也能保持数据一致性(进而实现分布式控制) 本文总结的分布式锁、限流操作都基于redis实现 分布式锁:利用redis的key-value存储结构,判断key是否存在,key在即锁在 限流操作:利用redis的原子性,通过increment/decrement方法进行计数,超出则抛出异常,终止访问。
三、Spring Boot的实现方式
这里的Spring Boot可以理解为微服务中的一个子服务 以下实现是编码过程,运行效果见第四部分,核心实现见本节第3、5部分
1. Reids配置和服务实现
1.1 redis配置
server.port = 8080
spring.redis.host = localhost
spring.redis.database = 0
spring.redis.port = 6379
package tech. xujian. lock. distributed. lockdistributed. config ; 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. Jackson2JsonRedisSerializer ;
import org. springframework. data. redis. serializer. StringRedisSerializer ; @Configuration
public class RedisConfig { @Bean public RedisTemplate < String , Object > redisTemplate ( RedisConnectionFactory redisConnectionFactory) { RedisTemplate < String , Object > redisTemplate = new RedisTemplate < > ( ) ; redisTemplate. setConnectionFactory ( redisConnectionFactory) ; redisTemplate. setKeySerializer ( new StringRedisSerializer ( ) ) ; redisTemplate. setValueSerializer ( new Jackson2JsonRedisSerializer < Object > ( Object . class ) ) ; redisTemplate. setHashKeySerializer ( new StringRedisSerializer ( ) ) ; redisTemplate. setHashValueSerializer ( new StringRedisSerializer ( ) ) ; return redisTemplate; }
}
1.2 redis服务实现
1.3 RedisService
package tech. xujian. lock. distributed. lockdistributed. service ; public interface RedisService { void set ( String k, String value) ; void set ( String k, String value, int expireMinute) ; String get ( String k) ; long getLong ( String k) ; long increment ( String k) ; long decrement ( String k) ; String getAndDelete ( String k) ;
}
1.4 RedisServiceImpl
package tech. xujian. lock. distributed. lockdistributed. service. impl ; import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. data. redis. core. RedisTemplate ;
import org. springframework. stereotype. Service ;
import tech. xujian. lock. distributed. lockdistributed. service. RedisService ; import java. util. concurrent. TimeUnit ; @Service
public class RedisServiceImpl implements RedisService { @Autowired RedisTemplate < String , String > redisTemplate; @Override public void set ( String k, String value) { redisTemplate. opsForValue ( ) . set ( k, value) ; } @Override public void set ( String k, String value, int expireMinute) { redisTemplate. opsForValue ( ) . set ( k, value, expireMinute, TimeUnit . MINUTES ) ; } @Override public String get ( String k) { return redisTemplate. opsForValue ( ) . get ( k) ; } @Override public long getLong ( String k) { String str = get ( k) ; return str == null ? 0 : Long . parseLong ( str) ; } @Override public long increment ( String k) { return redisTemplate. opsForValue ( ) . increment ( k) ; } @Override public String getAndDelete ( String k) { return redisTemplate. opsForValue ( ) . getAndDelete ( k) ; } @Override public long decrement ( String k) { return redisTemplate. opsForValue ( ) . decrement ( k) ; }
}
2 注解实现
2.1 锁类型枚举
package tech. xujian. lock. distributed. lockdistributed. annotation ; public enum RedisLockType { IP ( 1 ) , USERNAME ( 2 ) , COUNT ( 3 ) , ANY ( 4 ) ; private int value; RedisLockType ( int value) { this . value = value; } public int getValue ( ) { return value; }
}
2.2 注解声明
package tech. xujian. lock. distributed. lockdistributed. annotation ; 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 RedisLock { RedisLockType type ( ) default RedisLockType . IP ;
}
3 拦截去实现
3.1拦截器配置
package tech. xujian. lock. distributed. lockdistributed. config ; import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. context. annotation. Configuration ;
import org. springframework. stereotype. Component ;
import org. springframework. web. servlet. config. annotation. InterceptorRegistry ;
import org. springframework. web. servlet. config. annotation. WebMvcConfigurer ;
import tech. xujian. lock. distributed. lockdistributed. interceptor. RedisLockInterceptor ; @Component
@Configuration
public class WebConfig implements WebMvcConfigurer { @Autowired RedisLockInterceptor redisLockInterceptor; @Override public void addInterceptors ( InterceptorRegistry registry) { registry. addInterceptor ( redisLockInterceptor) . addPathPatterns ( "/**" ) ; }
}
3.2拦截器实现
package tech. xujian. lock. distributed. lockdistributed. interceptor ; import cn. hutool. core. util. StrUtil ;
import cn. hutool. extra. servlet. ServletUtil ;
import lombok. extern. slf4j. Slf4j ;
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Component ;
import org. springframework. util. Assert ;
import org. springframework. web. method. HandlerMethod ;
import org. springframework. web. servlet. HandlerInterceptor ;
import tech. xujian. lock. distributed. lockdistributed. annotation. RedisLock ;
import tech. xujian. lock. distributed. lockdistributed. annotation. RedisLockType ;
import tech. xujian. lock. distributed. lockdistributed. service. RedisService ; import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ; @Component
@Slf4j
public class RedisLockInterceptor implements HandlerInterceptor { @Autowired RedisService redisService; private static final String CACHE_KEY_REDIS_LOCK = "system:distributed:lock:" ; @Override public boolean preHandle ( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ( handler instanceof HandlerMethod ) { HandlerMethod handlerMethod = ( HandlerMethod ) handler; RedisLock redisLock = handlerMethod. getMethodAnnotation ( RedisLock . class ) ; if ( redisLock == null ) { return HandlerInterceptor . super . preHandle ( request, response, handler) ; } switch ( redisLock. type ( ) ) { case IP : doIpLock ( request) ; break ; case USERNAME : break ; case COUNT : doCountLock ( handlerMethod) ; break ; case ANY : break ; } } return HandlerInterceptor . super . preHandle ( request, response, handler) ; } @Override public void afterCompletion ( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { if ( handler instanceof HandlerMethod ) { HandlerMethod handlerMethod = ( HandlerMethod ) handler; RedisLock redisLock = handlerMethod. getMethodAnnotation ( RedisLock . class ) ; if ( redisLock == null ) { HandlerInterceptor . super . afterCompletion ( request, response, handler, ex) ; return ; } switch ( redisLock. type ( ) ) { case IP : releaseIpLock ( request) ; break ; case USERNAME : break ; case COUNT : releaseCountLock ( handlerMethod) ; break ; case ANY : break ; } } HandlerInterceptor . super . afterCompletion ( request, response, handler, ex) ; } private void doCountLock ( HandlerMethod handlerMethod) { String redisKey = CACHE_KEY_REDIS_LOCK + "count:" + handlerMethod. getMethod ( ) . getDeclaringClass ( ) + ":" + handlerMethod. getMethod ( ) . toGenericString ( ) ; redisKey = redisKey. replaceAll ( " " , "" ) ; log. info ( redisKey) ; long countNow = redisService. getLong ( redisKey) ; log. info ( "当前方法访问人数:" + countNow) ; Assert . isTrue ( countNow < 10 , "系统拥堵,请稍后重试!当前访问人数:" + countNow) ; redisService. increment ( redisKey) ; } private void releaseCountLock ( HandlerMethod handlerMethod) { String redisKey = CACHE_KEY_REDIS_LOCK + "count:" + handlerMethod. getMethod ( ) . getDeclaringClass ( ) + ":" + handlerMethod. getMethod ( ) . toGenericString ( ) ; redisKey = redisKey. replaceAll ( " " , "" ) ; long countNow = redisService. decrement ( redisKey) ; log. info ( "当前方法访问人数:" + countNow) ; } private void doIpLock ( HttpServletRequest request) { String ip = ServletUtil . getClientIP ( request) ; String redisKey = CACHE_KEY_REDIS_LOCK + "ip:" + ip; String value = redisService. get ( redisKey) ; if ( StrUtil . isEmpty ( value) ) { redisService. set ( redisKey, ip, 1 ) ; return ; } else { throw new RuntimeException ( "操作太快,请稍后重试" ) ; } } private void releaseIpLock ( HttpServletRequest request) { String ip = ServletUtil . getClientIP ( request) ; String redisKey = CACHE_KEY_REDIS_LOCK + "ip:" + ip; redisService. getAndDelete ( redisKey) ; } }
4 全局异常拦截
package tech. xujian. lock. distributed. lockdistributed. exception ; import org. springframework. web. bind. annotation. ControllerAdvice ;
import org. springframework. web. bind. annotation. ExceptionHandler ;
import org. springframework. web. bind. annotation. ResponseBody ; @ControllerAdvice
public class RedisLockExceptionHandler { @ExceptionHandler ( value = Exception . class ) @ResponseBody public String exceptionHandler ( Exception e) { e. printStackTrace ( ) ; return e. getMessage ( ) ; }
}
5 Controller上进行注解
示例如下,一看就懂 需要加锁/限流的方法,只需要添加一个注解就像了
@RestController
@RequestMapping ( "/lock" )
public class LockController { @Autowired RedisService redisService; @RedisLock ( type = RedisLockType . IP ) @GetMapping ( "/test/ip" ) public String lockTest ( ) throws InterruptedException { Thread . sleep ( 20000 ) ; return "succeed." ; } @RedisLock ( type = RedisLockType . COUNT ) @GetMapping ( "/test/count" ) public String testCount ( ) throws InterruptedException { Thread . sleep ( 20000 ) ; return "succeed." ; }
}
四、运行效果
上文controller中使用sleep使接口卡顿20秒,用来示意接口访问需要时间
1. 正常访问
访问中 访问结束后
2. 锁
如果在访问的时候再次发起访问,则提示错误(前者访问继续执行)
3. 限流
超出流量限制时: 本文的demo地址:https://gitee.com/rederxu/lock_distributed.git