基于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