今日学习:通用redis分布式缓存注解|ZipKin|Redisson源码Mermind图|

链路追踪Zipkin

​ 一个看起来很简单的应用,背后可能需要数十或数百个服务来支撑,一个请求就要多次服务调用。当请求变慢、或者不能使用时,我们是不知道是哪个后台服务引起的。这时,我们使用 Zipkin 就能解决这个问题。由于业务访问量的增大,业务复杂度增加,以及微服务架构和容器技术的兴起,要对系统进行各种拆分。微服务系统拆分后,我们可以使用 Zipkin 链路,来快速定位追踪有故障的服务点。

Zipkin/SkyWalking 是一款开源的分布式实时数据追踪系统(Distributed Tracking System),能够收集服务间调用的时序数据,提供调用链路的追踪。

Zipkin 其主要功能是聚集来自各个异构系统的实时监控数据,在微服务架构下,十分方便地用于服务响应延迟等问题的定位。

Zipkin 每一个调用链路通过一个 trace id 来串联起来,只要你有一个 trace id ,就能够直接定位到这次调用链路,并且可以根据服务名、标签、响应时间等进行查询,过滤那些耗时比较长的链路节点。

Zipkin 分布式跟踪系统就能非常好地解决该问题,主要解决以下3点问题:

    1. 动态展示服务的链路;
    1. 分析服务链路的瓶颈并对其进行调优;
    1. 快速进行服务链路的故障发现;

在优化前通过Zipkin链路追踪进行查看接口耗时

  1. service-util工具模块pom.xml中新增Zipkin相关依赖

    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-brave</artifactId>
    </dependency>
    <dependency>
        <groupId>io.zipkin.reporter2</groupId>
        <artifactId>zipkin-reporter-brave</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-observation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
        <version>2.2.8.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-micrometer</artifactId>
        <version>12.5</version>
    </dependency>
    
  2. 在Nacos配置common.yaml中新增配置(已完成

    management:
      zipkin:
        tracing:
          endpoint: http://192.168.200.6:9411/api/v2/spans
      tracing:
        sampling:
          probability: 1.0 # 记录速率100%
    
  3. 解决异步任务+多线程导致异步Feign请求无法被链路追踪,故需要未线程池设置装饰器(将当前线程内上下文中trace_id传递到子线程中),故将当日资料中复制到service-util且设置Spring线程池对象装饰器

      //设置解决zipkin链路追踪不完整装饰器对象
      taskExecutor.setTaskDecorator(new ZipkinTaskDecorator(zipkinHelper));
    
  4. 访问接口、或查看页面进行测试

  5. 通过Zipkin管理页面查看接口耗时

Redisson

EL表达式自定义缓存注解(通用)

RedissonConfig

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author : 戚欣扬
 * @Description :
 */
@Configuration
public class RedissonConfig {

    @Value("${spring.data.redis.host}")
    private String host ;
    @Value("${spring.data.redis.password}")
    private String pasword;
    @Value("${spring.data.redis.port}")
    private String port;

    private String timeout;
    private static String ADDRESS_PREFIX = "redis://";
    @Bean
    RedissonClient redissonSingle(){
        Config config = new Config();

        if (host.isEmpty()){
            throw  new RuntimeException("Redisson Host is empty!");
        }
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(ADDRESS_PREFIX + host + ":" + port);

        if (timeout!=null) {
            serverConfig.setTimeout(Integer.parseInt(timeout));
        }
        if (!pasword.isEmpty()){
            serverConfig.setPassword(pasword);
        }
        return Redisson.create(config);
    }
}

Json| RedisConfig

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;


/**
 * Redis配置类
 */
@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    @Primary
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        //String的序列化方式
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // 使用GenericJackson2JsonRedisSerializer 替换默认序列化(默认采用的是JDK序列化)
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

        //序列号key value
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

Annotation

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCacheEL {
    String prefixKey() default "";
    String suffixKey() default "";
    long expireTimeMills() default 7L;
    TimeUnit unit() default TimeUnit.DAYS;
}

Aspect

import com.fqxiny.aop01.annotation.MyCacheEL;
import jakarta.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Method;

@Aspect
@Component
public class ELCacheAspect {
    @Resource
    RedisTemplate redisTemplate;
    @Resource
    RedissonClient redissonClient;
    private final ExpressionParser parser = new SpelExpressionParser();
    private final StandardEvaluationContext context = new StandardEvaluationContext();
    private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new StandardReflectionParameterNameDiscoverer();

    @Around("@annotation(myCacheEL)")
    public Object doAround(ProceedingJoinPoint joinPoint, MyCacheEL myCacheEL) throws Throwable {
        // 获取方法签名和参数名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();

        // 获取参数名称数组(需要编译时保留 -parameters)
        String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);

        // 创建 EvaluationContext 并注册参数
        StandardEvaluationContext evalContext = new StandardEvaluationContext();
        if (paramNames != null && args != null) {
            for (int i = 0; i < paramNames.length; i++) {
                evalContext.setVariable(paramNames[i], args[i]);
            }
        }

        // 解析 suffixKey 中的 SpEL 表达式(如 #id)
        String suffixKey = myCacheEL.suffixKey();
        Object suffixValue = parser.parseExpression(suffixKey).getValue(evalContext, Object.class);

        // 构造最终缓存 key
        String bussinessKey = myCacheEL.prefixKey() + suffixValue;

        // 先从缓存取值
        Object result = redisTemplate.opsForValue().get(bussinessKey);
        if(result != null && ObjectUtils.isEmpty(result)){
            return result;
        }
        // 执行原方法,从数据库取值(先用Redisson加锁)
        // 获取分布式锁key
        String lockKey= bussinessKey+UUID.randomUUID();
        RLock lock = redissonClient.getLock(lockKey);
        //除了lock()都不会触发看门狗续期
        if(lock.tryLock()){
            try {
                result = joinPoint.proceed();
                redisTemplate.opsForValue().set(bussinessKey, result, myCacheEL.expireTimeMills(), myCacheEL.unit());
                return result;
            } catch (Throwable e) {
                throw new RuntimeException(e);
            } finally {
            //释放锁
                lock.unlock();
            }
        }else{
        //自旋,可能已被放入缓存中。
           return  this.doAround(joinPoint, myCacheEL);
        }

    }
}

Redisson源码Mermind图

在这里插入图片描述

为什么脚本快的同时还能保证原子性?


✅ 为什么脚本能 既快又原子

🧠 核心原因:Lua 脚本在 Redis 中是单线程原子执行的

Redis 是一个基于 单线程事件循环(Event Loop) 的内存数据库。这意味着:

  • 所有命令(包括 Lua 脚本)都是串行执行的;
  • 没有多线程并发访问共享资源的问题;
  • 因此天然具备 原子性

同时,因为 Redis 是纯内存操作 + 单线程,没有上下文切换和锁竞争,所以执行速度也非常快。


🔍 举个例子说明“原子性”的重要性

假设我们要实现一个简单的功能:

如果 key counter 存在且小于 10,则自增 1;否则设为 1。

如果用两个命令来做:

GET counter
# 判断是否小于10
INCR counter

在多客户端并发请求时就会出现 竞态条件(Race Condition),比如:

时间客户端 A客户端 B
T1GET counter = 9
T2GET counter = 9
T3INCR → 10INCR → 11 ❌

结果变成 11,超过了预期的 10。

但如果使用 Lua 脚本:

local current = redis.call('GET', KEYS[1])
if current and tonumber(current) < 10 then
    return redis.call('INCR', KEYS[1])
else
    return redis.call('SET', KEYS[1], 1)
end

这个脚本会作为一个整体被 Redis 执行,中间不会被打断,从而保证了 原子性


🧩 那么脚本为什么还能“快”?

1. 减少网络往返

  • 多条命令 → 一次脚本调用
  • 减少了网络延迟带来的性能损耗

2. 避免序列化/反序列化

  • Redis 命令需要多次解析参数;
  • Lua 脚本可以复用,Redis 支持缓存脚本 SHA1 哈希,直接传哈希执行更快。

3. 纯内存操作

  • Redis 数据都在内存中,Lua 脚本直接操作内存数据结构;
  • 不像传统数据库那样要走磁盘 IO、事务日志等流程。

4. 编译优化

  • Redis 内部对 Lua 脚本做了轻量级优化;
  • 脚本加载后会被缓存,下次只需传 SHA1。

⚠️ 注意事项:脚本不是万能的!

虽然 Lua 脚本很快也很原子,但你不能滥用:

❗ 不推荐场景:

  • 脚本太长或太复杂(会阻塞整个 Redis)
  • 执行时间过长(影响其他请求响应)

✅ 推荐场景:

  • 原子性操作(如计数器、分布式锁)
  • 多命令组合逻辑(避免竞态条件)
  • 性能关键路径的小型业务逻辑

📌 总结一句话:

脚本之所以快,是因为它减少了网络通信、绕过了序列化开销,并且直接操作内存数据;而它的原子性则来源于 Redis 的单线程模型,所有脚本都串行执行,不会有并发干扰。


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~Yogi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值