-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Any example using Redis #160
Comments
i'm wondering the same, although i have found some example by doing it programatically and only with Redisson. I'd prefered to do it with lettuce due to ACL and permissions problems with AWS and some additional config that my team and me had to do with the VPC and the access connection via Role-Based Access Control (RBAC). May this can help u: What im using:
dependencies: implementation 'org.redisson:redisson-spring-boot-starter:3.20.1'
implementation 'com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:0.9.0' as i said, i'd rather to do it with Lettuce Connection, and not with Redisson, but sadly spring does not provide a bean of import com.giffing.bucket4j.spring.boot.starter.config.cache.SyncCacheResolver;
import com.giffing.bucket4j.spring.boot.starter.config.cache.jcache.JCacheCacheResolver;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import io.github.bucket4j.grid.jcache.JCacheProxyManager;
import org.redisson.config.Config;
import org.redisson.config.ReadMode;
import org.redisson.jcache.configuration.RedissonConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import javax.cache.CacheManager;
import javax.cache.Caching;
import java.math.BigInteger;
import java.time.Duration;
import java.util.Collections;
import java.util.function.BinaryOperator;
@Configuration
@EnableCaching
public class RedisCacheRateLimitConfig{
private static final String CACHE_NAME = "r_quota_";
@Bean
public Config envConfig(SecretsService secrets) throws JsonProcessingException {
var port = secrets.get("port");
var host = secrets.get("master_node");
var hostread = secrets.get("replica_node");
var username = secrets.get("username");
var pass = secrets.get("password");
BinaryOperator<String> getUrl = (h, p) -> String.format("redis://%s:%s", h, p);
Config config = new Config();
config.useReplicatedServers()
.setReadMode(ReadMode.SLAVE)
.setUsername(username)
.setPassword(pass)
.addNodeAddress(getUrl.apply(host, port))
.addNodeAddress(getUrl.apply(hostread, port));
return config;
}
@Bean(name = "javaxCacheManager") // naming this due to spring uses a bean named cacheManager so it cannot rise up some troubles
public CacheManager cacheManager(Config config) {
return Caching.getCachingProvider().getCacheManager();
}
@Bean
public ProxyManager<String> proxyManager(CacheManager cache) {
return new JCacheProxyManager<>(cache.getCache(CACHE_NAME));
}
// this rises an exception on start up since there is no resolver when there is a reactive stack to handle the app (netty, tomcat3 etc) when there are several cache names.
@Bean
@Primary
public SyncCacheResolver bucket4jCacheResolver(CacheManager cacheManager) {
return new JCacheCacheResolver(cacheManager);
}
/** this bean creates the cache where the keys are being associated to your bucket. im using here the customizer
* so if there is a spring bean which uses some redis connection or caching, can share the same config without rising up
* the exception
*
*/
@Bean
public JCacheManagerCustomizer jCacheManagerCustomizer(Config config) {
return cacheManager -> cacheManager.createCache(CACHE_NAME, RedissonConfiguration.fromConfig(config));
}
} once you get the connection you can just create the resolveBucket to handle your service. in my case i used some configs based on Baeldung post - Rate Limit with Bucket4j import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.Refill;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.function.Supplier;
@Service
public class RateLimitService {
private static final int QUOTA = 20;
private final ProxyManager<String> buckets;
public RateLimitService(ProxyManager<String> buckets) {
this.buckets = buckets;
}
public Bucket resolveBucket(String key) {
Supplier<BucketConfiguration> confSupplier = () -> {
var refill = Refill.intervally(QUOTA, Duration.ofHours(1));
var limit = Bandwidth.classic(QUOTA, refill);
return BucketConfiguration.builder()
.addLimit(limit)
.build();
};
return buckets.builder().build(key, confSupplier);
}
} then i use a Web Filter to handle request instead of an Interceptor, so i can order my filters with spring security's SecurityFilterChain. import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.math.BigInteger;
@Component //if u use this, it will be handler as global filter and will intercept every request
public class RateLimiterFilter extends OncePerRequestFilter {
private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining";
private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Seconds";
private static final int NANO_SECONDS = 1_000_000_000;
private final RateLimitService rateLimitService;
public RateLimiterFilter(RateLimitService rateLimitService) {
super();
this.rateLimitService = rateLimitService;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
var key= request.getRemoteAddr();
var bucket = rateLimitService.resolveBucket(key);
var tokenConsumed = bucket.tryConsumeAndReturnRemaining(BigInteger.ONE.longValue());
if (tokenConsumed.isConsumed()) {
response.setHeader(HEADER_LIMIT_REMAINING, String.valueOf(tokenConsumed.getRemainingTokens()));
filterChain.doFilter(request, response);
} else {
long waitToRefill = tokenConsumed.getNanosToWaitForRefill() / NANO_SECONDS;
response.setContentType("application/json");
response.addHeader(HEADER_LIMIT_REMAINING, String.valueOf(tokenConsumed.getRemainingTokens()));
response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitToRefill));
response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota");
}
}
} hope it helps. EDIT: spelling |
Hi @dgallego58 |
I've updated the examples. Any suggestions for improvement are welcome. |
I am looking for an example using Redis.
Thank you.
The text was updated successfully, but these errors were encountered: