『技术派实战』第三期:并发利器缓存和限流

216 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

往期回顾

『技术派实战』第一期:我的网站上线了

『技术派实战』第二期:工程结构和技术选型

前言

目前为止,技术派还是一个单体架构的网站,虽然并发量还很小,完全没达到单台MySQL数据库实例(瓶颈一般都在数据库上)几千QPS的极限。但是凡是都讲究一个未雨绸缪,况且提供更好的用户体验也是我们技术人不懈的追求。

缓存

用缓存,主要有两个用途:高性能高并发

高性能

一个操作去DB查,可能要几百毫秒甚至几秒,直接去内存中通过key-value结构去查,只要几毫秒到几十毫秒,性能极大的提升。

对于很少变更的数据、读多写少的数据就很适合放到缓存中,提升用户响应速度。

高并发

查询的速度变快,是不是单位时间内,能查询的次数就变多了呢?

Redis

以Springboot+maven的项目为例,配置和使用都很容易入手:

  1. 添加Maven依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 添加项目配置(yml)
spring:
    redis:
      # springboot 2.x 默认使用lettuce做redis 客户端
      lettuce:
        # 这里是连接池配置,可以不配置,使用默认的参数
        pool:
          max-active: 100
          max-idle: 10
          min-idle: 5
          max-wait: -1
      host: xxx.xxx.xxx.xxx
      port: 6379
      password: xxxxx
  1. 使用RedisTemplate操作
/**
 * 先自动装配上RedisTemplate对象
 */
@Autowired
RedisTemplate redisTemplate;

// redis string 类型的插入数据,带有过期时间
redisTemplate.opsForValue().set("name","l拉不拉米",10, TimeUnit.MINUTES);

// redis string 类型的查询数据
redisTemplate.opsForValue().get("name");

本地缓存

使用Guava Cache工具库,同类型的还有Spring Cache。仅限于单机部署。

// 实例化一个cache对象,并设置过期时间
private Cache<String, List<ArticleVo>> articleCache = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();

/**
 * 文章点赞排名
 *
 * @return
 */
public List<ArticleVo> getTopLikeArticle() {
    // 先从本地缓存获取数据
    List<ArticleVo> topLikeCache = topArticleCache.getIfPresent(ApiConstant.TOP_LIKE_CACHE);
    if (CollUtil.isNotEmpty(topLikeCache)) {
        return topLikeCache;
    }
    // 如果缓存没有就从数据库查
    List<ArticleVo> topLikeArticles = getTopLikeArticles();
    // 重新放到缓存中
    topArticleCache.put(ApiConstant.TOP_LIKE_CACHE, topLikeArticles);
    return topLikeArticles;
}

限流

使用Guava RateLimiter工具库。仅限于单机部署。

技术派使用自定义注解+切面编程的方式实现。

  1. 先定义一个注解
/**
 * 限流注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    /**
     * 每秒向桶放入令牌的数量
     */
    double capacity() default 1.0;

    /**
     * 限流对象的名称
     */
    String name() default "rateLimiter";
}
  1. 定义限流切面
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
    /**
     * 限流容器
     */
    private ConcurrentHashMap<String, RateLimiter> RATE_LIMITER_CONTAINER = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.techpai.web.annotation.RateLimit)")
    private void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        //获取拦截的方法名
        Signature sig = point.getSignature();
        //获取拦截的方法名
        MethodSignature msig = (MethodSignature) sig;
        //返回被织入增加处理目标对象
        Object target = point.getTarget();
        //为了获取注解信息
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        //获取注解信息
        RateLimit annotation = currentMethod.getAnnotation(RateLimit.class);
        // 获取注解每秒加入桶中的token
        double capacity = annotation.capacity();
        // 注解所在方法名区分不同的限流策略
        String name = msig.getName();
        RateLimiter rateLimiter;
        if (RATE_LIMITER_CONTAINER.containsKey(name)) {
            rateLimiter = RATE_LIMITER_CONTAINER.get(name);
        } else {
            RATE_LIMITER_CONTAINER.put(name, RateLimiter.create(capacity));
            rateLimiter = RATE_LIMITER_CONTAINER.get(name);
        }
        if (rateLimiter.tryAcquire()) {
            log.info("{}-{}:限流处理完成", name, capacity);
            return point.proceed();
        } else {
            return "访问超过限流限制,请稍后再试";
        }
    }
}
  1. 使用限流
// 在方法上加上自定义注解
// guava rateLimiter使用的令牌桶,capacity表示每秒向桶内放入的令牌数,即每秒能处理的请求数,多的请求直接拒绝或等待(在切面中根据自己的业务场景确定)
@RateLimit(capacity = 10,name = "rss")

在之前的文章中,笔者也写过两篇文章讲解四种主流的限流算法,有兴趣的同学可以看看。

『超级架构师』图码实战限流算法(一)

『超级架构师』图码实战限流算法(二)

肯定会有同学要问,在分布式的场景下如何做限流呢?

笔者提供两个主流的方案:

  1. 阿里巴巴的开源分布式高可用流量组件:Sentinel
  2. Redis实现:用Lua脚本写限流的实现逻辑,通过redis客户端调用Lua脚本

下期预告

在下一期中,详细讲解技术派-Github趋势功能的实现。

image.png

喜欢的同学请多多点赞,多多收藏我的网站 www.jspai.cc 哟😇😇