当前位置: 首页 > news >正文

MyBatis 缓存体系剖析

一、MyBatis 缓存体系基础:先搞懂这两个核心概念

1.1 一级缓存:SqlSession 内的"临时缓存"

作用范围

  • 仅限于当前 SqlSession 实例
  • 不同 SqlSession 之间的缓存相互隔离
  • 示例:用户 A 和用户 B 同时使用不同 SqlSession 查询相同数据,会各自维护独立的一级缓存

生命周期

  • 随 SqlSession 的创建而初始化
  • 随 SqlSession 的 close()、commit() 或 rollback() 操作而清空
  • 典型场景:在事务中多次查询相同数据时,只有第一次会访问数据库

触发逻辑

  • MyBatis 默认开启一级缓存,无需额外配置
  • 执行流程:
    1. 执行相同 SQL 查询(相同的 SQL 语句 + 相同的参数)
    2. 第一次查询从数据库获取数据并写入一级缓存
    3. 后续查询直接从缓存中读取,避免重复数据库交互

核心原理

  • 底层数据结构:SqlSession 内部维护的 HashMap
  • 缓存键(Key)组成:
    • MappedStatement 的 ID(Mapper 接口方法的全限定名)
    • SQL 语句
    • 参数值
    • 分页信息(RowBounds)
    • 环境 ID(Environment ID)
  • 缓存值(Value):查询结果对象

注意事项

  • 执行 insert/update/delete 操作会自动清空当前 SqlSession 的一级缓存
  • 可以调用 clearCache() 方法手动清空一级缓存
1.2 二级缓存:跨 SqlSession 的"共享缓存"

作用范围

  • 跨 SqlSession 共享
  • 属于 Mapper 接口级别的缓存(同一个 Mapper.xml 或 @Mapper 接口下的所有方法共享)
  • 示例:不同用户通过不同 SqlSession 访问相同 Mapper 方法时,可以共享缓存数据

生命周期

  • 随 SqlSessionFactory 的创建而初始化
  • 默认会一直存在,除非:
    • 手动清空(调用 clearCache())
    • 配置了过期策略(如 LRU 淘汰)
    • 执行了对应的修改操作(insert/update/delete)

开启条件

  1. 全局配置中开启二级缓存(MyBatis 3.4.0+ 默认开启):
    <settings><setting name="cacheEnabled" value="true"/>
    </settings>
    

  2. 在具体 Mapper 中启用:
    • XML 方式:在 Mapper.xml 中添加 <cache/> 标签
    • 注解方式:在 @Mapper 接口上添加 @CacheNamespace 注解

核心原理

  • 底层实现:Cache 接口的实现类(默认是 PerpetualCache,本质也是 HashMap)
  • 管理方式:由 Mapper 接口对应的 Configuration 对象管理
  • 数据同步流程:
    1. SqlSession 提交时,会将当前 SqlSession 的一级缓存数据同步到二级缓存
    2. 其他 SqlSession 查询相同数据时:
      • 先检查二级缓存
      • 不存在则检查一级缓存
      • 最后才访问数据库

缓存策略配置

<cacheeviction="FIFO"  <!-- 淘汰策略:FIFO/LRU/SOFT/WEAK -->flushInterval="60000" <!-- 刷新间隔(毫秒) -->size="512" <!-- 缓存对象数量 -->readOnly="true" <!-- 是否只读 -->
/>

注意事项

  • 实体类必须实现 Serializable 接口
  • 查询结果越大,二级缓存占用的内存越多
  • 分布式环境下需要考虑缓存一致性问题
  • 对于频繁修改的数据,建议关闭二级缓存

二、一级缓存的 “隐形坑”:90% 开发者都踩过的问题

2.1 问题 1:多线程共享 SqlSession 导致的数据不一致

场景复现与详细分析

在传统 Web 项目中,常见错误做法是在 DAO 层使用如下代码模式:

public class UserDao {private static SqlSession sqlSession = MybatisUtil.getSqlSession(); // 错误示例:静态变量持有SqlSessionpublic User getById(int id) {return sqlSession.selectOne("userMapper.getById", id);}
}

当多个HTTP请求同时访问时(如用户A访问个人中心页面,用户B同时修改个人资料),线程A通过getById查询到的可能仍是修改前的旧数据,因为:

  1. 线程B执行update操作后,数据库实际数据已变更
  2. 但线程A的SqlSession一级缓存未感知这个变更
  3. 线程A后续相同查询继续返回缓存结果

问题根源的深层解析

MyBatis架构设计中:

  • SqlSession相当于JDBC的Connection+Statement组合体
  • 一级缓存本质是PerpetualCache实例(HashMap实现)
  • 缓存键由MapperId+SQL+参数值等要素构成

线程安全问题具体表现为:

  1. HashMap本身非线程安全,并发读写可能导致死循环
  2. 即使使用ConcurrentHashMap,业务逻辑上也不应共享缓存视图
  3. 事务隔离级别无法作用在同一SqlSession的不同线程上

解决方案的完整实践

方案一:Spring集成最佳实践(推荐)
<!-- spring-mybatis配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="dataSource"/>
</bean><bean class="org.mybatis.spring.SqlSessionTemplate"><constructor-arg ref="sqlSessionFactory"/><!-- 关键配置:每个请求新建SqlSession --><constructor-arg value="SIMPLE"/> 
</bean>

方案二:手动管理场景示例
try (SqlSession session = sqlSessionFactory.openSession()) {UserMapper mapper = session.getMapper(UserMapper.class);User user = mapper.getById(1);// 执行更新后必须清空缓存mapper.updateName(1, "newname");session.clearCache();  // 关键操作// 再次查询将获取最新数据User updatedUser = mapper.getById(1);
}

2.2 问题 2:事务内多次查询的 "脏读" 风险

典型业务场景示例

电商订单处理流程:

@Transactional
public void processOrder(int orderId) {// 第一次查询订单状态Order order = orderMapper.selectById(orderId); // 状态为"未支付"// 业务逻辑处理...orderMapper.updateStatus(orderId, "已支付");// 第二次查询(错误期望获取"已支付"状态)Order currentOrder = orderMapper.selectById(orderId); // 实际可能仍返回"未支付"状态!
}

缓存机制原理图

事务开始│├─ SELECT操作 → 存入一级缓存│├─ UPDATE操作 → 仅标记脏数据(不立即清空)│└─ 再次SELECT → 优先读取缓存(可能脏数据)
事务提交 → 清空关联缓存

解决方案对比

方案实现方式优点缺点适用场景
手动清缓存sqlSession.clearCache()精确控制需修改多处代码关键业务逻辑
flushCache@Options(flushCache=true)声明式配置性能损耗强一致性要求
分离查询将查询移出事务彻底避免架构改动大读写分离架构

2.3 问题 3:分页查询的缓存 "失效" 假象

缓存键生成机制详解

MyBatis缓存键组成要素:

  1. Mapper方法全限定名
  2. 分页参数(offset+limit)
  3. 参数对象hashCode
  4. 环境ID(environment)

示例缓存键对比:

第一次查询(page=1):
UserMapper.getByPage::1:10:0:12345:development第二次查询(page=2):
UserMapper.getByPage::10:10:0:12345:development

分页插件协作原理

PageHelper执行流程:

  1. 拦截Executor.query()方法
  2. 解析ThreadLocal中的分页参数
  3. 改写SQL添加LIMIT子句
  4. 参数最终成为缓存键组成部分

二级缓存配置示例

<cache eviction="LRU" flushInterval="60000"size="512" readOnly="true"/><select id="getByPage" resultType="User" useCache="true">select * from users
</select>

性能优化建议

  1. 热点数据分页缓存策略:

    • 第一页缓存时间较短(如1分钟)
    • 后续页缓存时间较长(如10分钟)
  2. 缓存更新策略:

    @CacheEvict(value="userPageCache", allEntries=true)
    public void updateUser(User user) {userMapper.update(user);
    }
    

  3. 特殊场景处理:

    • 实时性要求高的场景:添加flushCache=true
    • 海量数据分页:考虑使用游标分页代替传统分页

三、二级缓存的 “深水区”:数据一致性与性能的平衡难题

3.1 问题 1:多表关联查询的缓存脏数据

详细场景复现

假设我们有一个电商系统,包含用户和订单两个核心业务模块:

1.UserMapper 接口:

public interface UserMapper {@CacheNamespace // 开启二级缓存User getUserWithOrders(@Param("userId") Long userId);
}

2.OrderMapper 接口:

public interface OrderMapper {int updateOrderStatus(@Param("orderId") Long orderId, @Param("status") String status);
}

具体执行流程:

  1. 用户A访问个人中心页面,调用getUserWithOrders(1),查询结果包含:
    • 用户信息:ID=1,name="张三"
    • 订单信息:[{orderId=101, status="待支付"}]
  2. 该结果被缓存到UserMapper的二级缓存中
  3. 用户B(商家)通过后台系统调用updateOrderStatus(101, "已发货")成功更新订单状态
  4. OrderMapper的更新操作触发了自身二级缓存的清空
  5. 用户A刷新个人中心页面,再次调用getUserWithOrders(1),仍然从UserMapper的缓存中读取到旧的订单状态"待支付"

问题深入分析

MyBatis二级缓存的实现机制:

  • 每个Mapper拥有独立的缓存空间
  • 缓存键的生成规则:Mapper方法签名 + 参数值
  • 更新操作会清空当前Mapper的缓存空间
  • 关联查询的结果会完整缓存在发起查询的Mapper空间内

扩展解决方案

方案1优化:共享缓存空间的最佳实践
<!-- UserMapper.xml -->
<cache-ref namespace="com.example.mapper.CommonCache"/><!-- OrderMapper.xml --> 
<cache-ref namespace="com.example.mapper.CommonCache"/><!-- CommonCache.xml -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"><property name="timeToLiveSeconds" value="3600"/>
</cache>

注意事项:

  1. 建议创建专门的CommonCache作为共享空间
  2. 使用第三方缓存实现(如Ehcache)比内置缓存更稳定
  3. 需要评估共享缓存对性能的影响
方案2扩展:精确缓存清理的实现示例
public class CacheUtil {private final SqlSessionFactory sqlSessionFactory;public void clearMapperCache(String mapperClassName) {Cache cache = sqlSessionFactory.getConfiguration().getCache(mapperClassName);if (cache != null) {cache.clear();}}// 在OrderService中调用public void updateOrder(Order order) {orderMapper.update(order);cacheUtil.clearMapperCache("com.example.mapper.UserMapper");}
}

方案3补充:Redis缓存设计建议
  1. 缓存键设计:user:orders:{userId}
  2. 缓存更新策略:
@Transactional
public void updateOrder(Order order) {// 更新数据库orderMapper.update(order);// 删除关联缓存redisTemplate.delete("user:orders:" + order.getUserId());
}

3.2 问题 2:缓存穿透(Cache Penetration)

典型攻击场景

  1. 恶意请求示例:
    • /user/get?id=-1
    • /user/get?id=999999999(不存在的ID)
  2. 攻击特征:
    • 高频请求(1000+ QPS)
    • 参数明显不合法
    • 每次请求都会直达数据库

解决方案实现细节

方案1完整配置
<select id="getUserById" resultType="User" cacheNulls="true">SELECT * FROM user WHERE id = #{id}
</select>

缓存效果:

  • 查询id=1(存在):缓存User对象
  • 查询id=-1(不存在):缓存NULL值
  • 缓存时间与其他数据一致
方案3的布隆过滤器实现
public class UserBloomFilter {private final BloomFilter<Long> filter;public UserBloomFilter() {this.filter = BloomFilter.create(Funnels.longFunnel(), 1000000,  // 预期数据量0.01      // 误判率);}public void init(List<Long> userIds) {userIds.forEach(filter::put);}public boolean mightContain(Long userId) {return filter.mightContain(userId);}
}// 在查询前校验
public User getUser(Long id) {if (!bloomFilter.mightContain(id)) {return null;}return userMapper.getUserById(id);
}

3.3 问题 3:缓存雪崩(Cache Avalanche)

真实案例

某电商平台在每日凌晨2点出现服务不可用,经排查发现:

  • 200+个Mapper配置了相同的1小时缓存
  • 促销活动期间QPS达到5000+
  • 缓存同时失效导致数据库连接池耗尽

解决方案实施指南

方案1:时间随机化实现
public class RandomCacheDecorator implements Cache {private final Cache delegate;private final int randomRange; // 随机范围(分钟)public RandomCacheDecorator(Cache delegate, int randomRange) {this.delegate = delegate;this.randomRange = randomRange;}@Overridepublic void putObject(Object key, Object value) {// 原始过期时间60分钟 + 随机0-10分钟int randomOffset = new Random().nextInt(randomRange);int ttl = 60 + randomOffset; // 实际实现需要根据具体缓存组件调整delegate.putObject(key, value, ttl);}
}

方案2:多级缓存架构
请求流程:
1. 先查询MyBatis一级缓存(SqlSession级别)
2. 未命中则查询MyBatis二级缓存(Mapper级别) 
3. 仍未命中则查询Redis集群
4. 最后才访问数据库

各级缓存配置建议:

  • 一级缓存:默认开启,生命周期同SqlSession
  • 二级缓存:存活时间5-10分钟
  • Redis缓存:存活时间30分钟+随机偏移

3.4 问题 4:序列化异常(SerializedException)

典型错误堆栈

Caused by: org.apache.ibatis.cache.CacheException: Error serializing objectat org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:98)at org.apache.ibatis.cache.decorators.SerializedCache.putObject(SerializedCache.java:58)

深度解决方案

方案1的完整实现示例
public class User implements Serializable {private static final long serialVersionUID = 1L;private Long id;private String name;private transient String password; // 敏感字段不序列化// 嵌套对象也需要序列化private List<Address> addresses; // 自定义序列化逻辑private void writeObject(java.io.ObjectOutputStream out)throws IOException {out.defaultWriteObject();// 自定义处理...}
}

方案2的缓存配置详解
<cache type="org.mybatis.caches.ehcache.EhcacheCache"><!-- 使用LRU淘汰策略 --><property name="memoryStoreEvictionPolicy" value="LRU"/><!-- 最大缓存对象数 --><property name="maxEntriesLocalHeap" value="1000"/><!-- 不启用磁盘存储 --><property name="overflowToDisk" value="false"/>
</cache>

注意事项:

  1. 非序列化缓存不能跨JVM共享
  2. 分布式环境必须使用支持序列化的缓存方案
  3. 实体类变更时需要维护serialVersionUID

四、MyBatis 缓存问题排查:实用工具与方法论

4.1 开启 MyBatis 缓存日志

通过日志查看 MyBatis 缓存的命中、写入、清空过程,是定位缓存相关问题的核心手段。在日志配置文件中添加以下内容:

Logback 配置示例

<!-- 配置MyBatis缓存相关日志级别 -->
<logger name="org.apache.ibatis.cache" level="DEBUG"/>
<logger name="org.apache.ibatis.executor.CachingExecutor" level="DEBUG"/>
<logger name="org.apache.ibatis.executor.BaseExecutor" level="DEBUG"/>

log4j2 配置示例

<Loggers><Logger name="org.apache.ibatis.cache" level="DEBUG"/><Logger name="org.apache.ibatis.executor.CachingExecutor" level="DEBUG"/><Logger name="org.apache.ibatis.executor.BaseExecutor" level="DEBUG"/>
</Loggers>

配置后,日志中会输出以下关键信息:

  1. 缓存命中信息

    • Cache Hit Ratio [com.example.mapper.UserMapper]: 0.5:显示指定Mapper的缓存命中率(50%)
    • Cache Hit: select * from user where id = ?:表示查询命中缓存
  2. 缓存未命中信息

    • Cache Miss: select * from user where id = ?:表示未命中缓存,将执行数据库查询
    • Cache Put: select * from user where id = ?:将查询结果存入缓存
  3. 缓存清理信息

    • Cache Clear: com.example.mapper.UserMapper:表示该Mapper的缓存被清空
    • Cache Flush: com.example.mapper.UserMapper:表示该Mapper的缓存被刷新

4.2 缓存命中率监控

缓存命中率是衡量缓存有效性的核心指标。理想情况下,二级缓存命中率应保持在70%以上。若命中率过低(如低于30%),说明缓存策略需要优化。

监控方式1:自定义Cache实现类

创建自定义缓存类,扩展MyBatis的PerpetualCache,加入命中率统计功能:

public class MonitorCache extends PerpetualCache {private final AtomicLong hitCount = new AtomicLong(0);private final AtomicLong requestCount = new AtomicLong(0);public MonitorCache(String id) {super(id);}@Overridepublic Object getObject(Object key) {requestCount.incrementAndGet();Object value = super.getObject(key);if (value != null) {hitCount.incrementAndGet();}return value;}// 获取当前命中率public double getHitRatio() {return requestCount.get() == 0 ? 0 : (double) hitCount.get() / requestCount.get();}// 获取总请求次数public long getRequestCount() {return requestCount.get();}// 获取命中次数public long getHitCount() {return hitCount.get();}
}

在MyBatis配置中启用自定义缓存:

<cache type="com.yourpackage.MonitorCache"/>

监控方式2:使用Spring Boot Actuator

  • 添加依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency><groupId>io.micrometer</groupId><artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

  • 配置application.yml:
management:endpoints:web:exposure:include: health,metrics,prometheusmetrics:export:prometheus:enabled: true
  • 通过以下端点获取缓存指标:
    • /actuator/metrics/cache.gets:缓存获取次数
    • /actuator/metrics/cache.hits:缓存命中次数
    • /actuator/metrics/cache.miss:缓存未命中次数

监控方式3:集成Prometheus+Grafana

  • 配置Prometheus抓取应用指标:
scrape_configs:- job_name: 'spring-actuator'metrics_path: '/actuator/prometheus'static_configs:- targets: ['localhost:8080']

  • 在Grafana中创建仪表盘,监控以下关键指标:

    • 缓存命中率:cache_hits / cache_gets * 100
    • 缓存未命中率:cache_miss / cache_gets * 100
    • 缓存大小变化趋势
  • 设置告警规则,当命中率低于设定阈值(如30%)时触发告警

五、MyBatis 缓存最佳实践:避坑指南与性能优化

5.1 一级缓存最佳实践

SqlSession 线程私有规范

在 Spring 环境中,应当完全依赖 SqlSessionTemplate 自动管理 SqlSession 生命周期。开发者不应手动创建或长期持有 SqlSession 实例,因为:

  • Spring 的 SqlSessionTemplate 会为每个事务/方法调用创建新的 SqlSession
  • 手动创建的 SqlSession 可能导致线程安全问题
  • 未正确关闭的 SqlSession 会造成数据库连接泄漏

事务内缓存复用策略

当事务内部包含更新操作时,需要特别注意一级缓存的同步问题:

  1. 在更新操作(INSERT/UPDATE/DELETE)后调用 SqlSession#clearCache() 方法
  2. 特别是当事务中包含"先查询后更新"的逻辑时,必须清理缓存
  3. 示例场景:
@Transactional
public void updateProductPrice(Long productId, BigDecimal newPrice) {// 查询操作会填充一级缓存Product product = productMapper.selectById(productId);// 更新操作product.setPrice(newPrice);productMapper.update(product);// 必须清空缓存,确保后续查询获取最新数据sqlSession.clearCache();
}

分页查询缓存处理

MyBatis 的一级缓存会基于完整的 SQL 语句和参数生成缓存键,这意味着:

  • LIMIT 10 OFFSET 0LIMIT 10 OFFSET 10 会被视为不同的查询
  • 不需要特别处理分页查询的缓存问题
  • 应当避免试图"复用"不同分页的查询结果

5.2 二级缓存最佳实践

适用场景分析

二级缓存最适合以下业务场景:

  1. 系统配置表(如 sys_config)
  2. 数据字典表(如 sys_dict)
  3. 地区编码表(如 region_code)
  4. 其他变更频率低(每天更新≤3次)的基础数据表

多表关联处理方案

当涉及多表关联查询时,二级缓存的处理策略:

  1. 首选方案:避免使用二级缓存,改用业务层缓存
  2. 次选方案:通过 <cache-ref> 让关联的 Mapper 共享同一缓存空间
    <!-- OrderMapper.xml -->
    <cache-ref namespace="com.example.mapper.UserMapper"/>
    

  3. 备选方案:为关联查询设置较短的 flushInterval(如60秒)

精细化缓存配置

二级缓存的配置参数需要根据业务特点精心调整:

参数推荐值适用场景注意事项
evictionLRU绝大多数场景比FIFO更能提高命中率
flushInterval3600000配置类数据单位毫秒,0表示永不过期
size1000数据量中等监控内存使用情况
readOnlytrue只读数据提升性能但无法更新

大字段处理建议

对于包含大字段(如文章内容、产品详情)的查询:

  1. <select> 中设置 useCache="false"
  2. 或者配置单独的 DTO 只包含必要字段
  3. 或者使用延迟加载策略

5.3 分布式缓存整合方案

5.3.1 Redis 集成详细实现

依赖配置详解
<!-- 使用新版本以获取更好的性能和特性支持 -->
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.3.1</version>
</dependency>

Redis 高级配置
# redis.properties
redis.host=${REDIS_HOST:127.0.0.1}
redis.port=${REDIS_PORT:6379}
redis.password=${REDIS_PASSWORD:}
redis.database=${REDIS_DB:0}
redis.timeout=2000
redis.maxTotal=100
redis.maxIdle=20
redis.minIdle=5
redis.testOnBorrow=true

缓存策略优化
<cache type="org.mybatis.caches.redis.RedisCache"><!-- 使用更高效的序列化方案 --><property name="serializer" value="org.mybatis.caches.redis.serialize.JacksonRedisSerializer"/><!-- 动态过期时间 --><property name="defaultExpiration" value="1800"/><!-- 开启缓存预热 --><property name="preload" value="true"/>
</cache>

5.3.2 其他分布式方案

  1. Memcached 方案
    <dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-memcached</artifactId><version>1.1.0</version>
    </dependency>
    

  2. Caffeine 本地缓存(适合混合架构):
    <dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-caffeine</artifactId><version>1.1.0</version>
    </dependency>
    

5.4 缓存监控与调优

监控指标

  1. 命中率监控
    Cache cache = sqlSessionFactory.getConfiguration().getCache("com.example.mapper.UserMapper");
    if (cache instanceof PerpetualCache) {int hits = ((PerpetualCache) cache).getHitCount();int misses = ((PerpetualCache) cache).getMissCount();double hitRatio = (double)hits/(hits+misses);
    }
    

  2. 内存占用监控(通过JMX)

调优策略

  1. 当命中率<30%时,考虑:
    • 缩小缓存范围
    • 缩短过期时间
    • 完全禁用缓存
  2. 当命中率>70%但响应仍慢时,考虑:
    • 增加缓存容量
    • 优化序列化方案
    • 升级缓存服务器

5.5 特殊场景处理

批量操作缓存处理

@Transactional
public void batchUpdate(List<Product> products) {// 批量操作前清空缓存sqlSession.clearCache();for (Product product : products) {productMapper.update(product);}// 操作后再次清空确保一致性sqlSession.clearCache();
}

多级缓存方案

对于极高并发的系统,可以采用:

  1. 一级缓存(Session级)
  2. 二级缓存(Redis集群)
  3. 本地缓存(Caffeine) 通过 @Cacheable 注解实现多级缓存联动。

http://www.dtcms.com/a/392588.html

相关文章:

  • MySQL 主从复制 + MyCat 读写分离 — 原理详解与实战
  • Vmake AI:美图推出的AI电商商品图编辑器,快速生成AI时装模特和商品图
  • Debian13 钉钉无法打开问题解决
  • 02.容器架构
  • Diffusion Model与视频超分(1):解读淘宝开源的视频增强模型Vivid-VR
  • 通过提示词工程(Prompt Engineering)方法重新生成从Ollama下载的模型
  • 有没有可以检测反爬虫机制的工具?
  • 大模型为什么需要自注意力机制?
  • 长度为K子数组中的最大和-定长滑动窗口
  • Linux安装Kafka(无Zookeeper模式)保姆级教程,云服务器安装部署,Windows内存不够可以看看
  • WEEX编译|续写加密市场叙事
  • 为 Element UI 表格增添排序功能
  • 点评项目(Redis中间件)第四部分缓存常见问题
  • 动态水印也能去除?ProPainter一键视频抠图整合包下载
  • DevSecOps 意识不足会导致哪些问题
  • LeetCode:27.合并两个有序链表
  • 适用于双节锂电池的充电管理IC选型参考
  • 格式说明符
  • 层数最深叶子节点的和(深度优先搜索)
  • 【git】安装和基本指令
  • 如何利用AI技术快速生成专业级的PPT和视频内容
  • Linux系统之----线程互斥与同步
  • ARM SMMUv2架构下的安全和非安全状态(secure/non-secure)下的的资源分配解析
  • 面向linux新手的OrcaTerm AI 最佳实践
  • 构建高可用 LVS-DR + Keepalived 负载均衡集群实战指南
  • 网络协议总结
  • Python多线程爬虫加速电商数据采集
  • JVM之直接内存(Direct Memory)
  • 深入理解C指针(四):回调函数与qsort——指针实战的终极舞台
  • 翻拍图像检测(即拍摄屏幕的照片)功能实现思路