【得物技术】浅谈本地缓存与分布式缓存

引言

在互联网电商行业,由于订单履约物流等核心业务的特殊性,要求在保证业务正确性的基础上,这些链路的响应时间不能过高,否则会影响上下游的的其他业务。为了解决这个问题,一般的做法是尽可能利用缓存,减少直接访问DB来降低响应时间,但这种方案会导致读写缓存的策略变得更为复杂。大部分场景下大致流程如下:

缓存淘汰算法

在业务中大量使用缓存之前,有必要了解缓存相关原理及淘汰算法,合理使用,才能保障线上业务的稳定性。

简单来说,缓存会设置一个最大存储量,当缓存存储的数量超过最大空间时,为了保证在稳定服务的同时有效提升命中率,设计适合自身数据特征的淘汰算法能够有效提升缓存命中率。常见的淘汰算法有如下几种。

FIFO

FIF0(first in first out):即「先进先出」。最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。

这种算法适用于「保证高频数据有效性场景,优先保障最新数据可用」。

LFU

LFU(less frequently used):即「最少使用」。无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。

这种算法适用于「保证高频数据有效性场景」。

LRU

LRU(least recently used):即「最近最少使用」。无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。

这种算法适用于「热点数据场景,优先保证热点数据的有效性」。

 

本地缓存

本地缓存也可以理解为进程内的缓存,特点是速度快,不会受到网络阻塞的干扰,但是因为是存放在jvm的内存中(堆的老年代)所以容量较小且各个进程间的缓存不能共享,不会持久化。随着服务的重启,所占用的空间会释放掉,所以适用于缓存一些应用中基本不会变化又频繁会被访问的数据,比如物流行业维护的仓库地址等信息。

优势

本地缓存相对于IO操作速度快、效率高。Redis是一种优秀的分布式缓存实现,但受限于网卡等原因,远水救不了近火,因此DB + Redis + LocalCache = 高效存储&高效访问。访问速度和花费的关系如下图所示:

图片来源-知乎

技术实现

HashMap

使用hashmap是最简单基础方式,是JDK的自带类,因为其内部为KV结构,存储对应的键值对,在需要时,可以通过key获得相应的value,因为不是线程安全的类,所以在需要保证线程安全时可以使用ConcurrentHashMap。这种方式使用简单但是对象的有效性和周期性不能保证(也就是没有缓存淘汰算法),容易造成内存的急剧上升,适用于业务简单,数据量小且对并发量没有太高要求的场景。

/*** 定义全局变量*/private static Map<String,Object> CACHE_MAP;

/*** 定义全局变量*/private static Map<String,Object> CACHE_MAP;

 

/**
     * 定义全局变量
     */
    private static Map<String,Object> CACHE_MAP;

/**
 * 获取缓存map中的值
 * @param key
 * @return
 */
public static Object getCacheMapValue(String key){
    Object value = getCacheMap().get(key);
    return value == null ? null : value ;
}

/**
 * 存放键值对
 * @param key
 * @param value
 */
public static void putCache(String key,Object value){
    getCacheMap().put(key,value);

}

/**
 * 删除
 * @param key
 */
public static void removeCache(String key){
    getCacheMap().remove(key);
}

/**
 * 获取map
 * @return
 */
public static Map<String,Object> getCacheMap(){
    if(CACHE_MAP == null ){
        CACHE_MAP = new HashMap<>();
    }

    return CACHE_MAP;
}

public static void main(String[] args) {
    putCache("1","test1");
    putCache("2","test2");
    putCache("3","test3");

    System.out.println(getCacheMapValue("1")); //返回test1

    removeCache("1");

    System.out.println(getCacheMapValue("1")); //返回nul
}

GuavaCache

官方地址:https://github.com/google/guava/wiki/CachesExplained

Guava是google开源的一个公共java库,类似于Apache Commons,它提供了集合,反射,缓存,科学计算,xml,io等一些工具类库。cache只是其中的一个模块。使用GuavaCache能够方便快速的构建本地缓存。

特点

  • GuavaCache缓存的容器为Cache<K, V>接口的实现类,以K,V的形式存在,接口为泛型,可以支持不同类型的key和value;

  • 实现原理基于ConcurrentHashMap(jdk1.7之前Segment分离锁原理),所以能够保障线程安全,在多线程高并发的场景下能够保证程序中共享数据的准确性

  • 对于数据的回收与ConcurrentHashMap不同,ConcurrentHashMap会保留所有添加的元素,直到将它显性删除为止,GuavaCache提供了三种缓存回收方式(基于容量回收、定时回收和基于引用回收)来限制内存的占用量;

  • 封装了原子性的put/get的操作;

基本用法

先在pom文件中添加依赖:

<dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>25.0-jre</version>
</dependency>

构建缓存对象

GuavaCache的创建为建造者模式,每次调用方法后返回的是对象本身,可以直接通过CacheBuilder.build()的创建,以下为最简单的创建方式。

创建一个缓存对象,key和value均为String类型:

Cache<String,String> cache = CacheBuilder.newBuilder().build();
cache.put("word","Hello World");
System.out.println(cache.getIfPresent("word"));

在创建cache时设置过期时间、最大存储容量、并发级别等。

Cache<String,String> cache1 = CacheBuilder.newBuilder()
                .concurrencyLevel(8)                     //并发级别为8,表示只有8个线程可以同时写缓存
                .expireAfterWrite(10, TimeUnit.SECONDS) //写入缓存10秒后过期
                .initialCapacity(10)                             //容器初始容量为10
                .maximumSize(15)                                 //容器最大存储容量为15
                        .build();

注意:容器的最大存储容量maximumSize,设置了15但后续如果put的个数超过15,就会按照LRU算法来移除最近最少被使用的缓存项,例如指定为2,put了3个GuavaCache依照LRU算法就会默认移除第一个被put的数据。

 

缓存回收方式

GuavaCache提供了三种基本的回收方式,基于容量回收、定时回收和基于引用回收。基于容量回收就是指定容量后超过预设阀值就依照LRU算法进行回收,定时回收就是指定缓存失效时间。

  • 基于引用回收

可以通过weakKeys和weakValues指定GuavaCache中的key和value为弱引用,这样当垃圾回收器运行时若没有被其他强引用指定则会被回收,避免内存占用过多。

Cache<String,Object> cache3 = CacheBuilder.newBuilder()
                .weakKeys()
                .weakValues()
                .build();

注意:由于垃圾回收是依赖==进行判断是否相等,因此这样会导致整个缓存也会使用==来比较键的相等性,而不是使用equals();所以在利用缓存的key比较时需要用==,这一点官方文档也有说明。

  • 显式回收

除了设置过期时间来将时间范围内未被访问的缓存项删除之外,还能通过以下方法将缓存显式删除:

  • 使用Cache.invalidate(key)单个移除;

  • 使用Cache.invalidteAll(keys)批量移除;

  • 使用Cache.invalidateAll()移除全部。

 Cache<String,Object> cache4 = CacheBuilder.newBuilder().build();
        cache4.put("1","1");
        cache4.put("2","2");
        cache4.put("3","3");

//cache4.invalidate("1"); //单个删除key为1的缓存项

List<String> keyList=new ArrayList<>();
keyList.add("1");
keyList.add("2");
cache4.invalidateAll(keyList); //将key为1和2的缓存项批量移除

cache4.invalidateAll(); //将cache4中的缓存项全部移除

两种缓存接口

Cache

Cache是通过CacheBuilder的build()方法构建,它是Gauva提供的最基本的缓存接口,并且它提供了一些常用的缓存api,上面介绍的就是基于Cache。

LoadingCache

LoadingCache是Cache的子类,它能够通过CacheLoader自发的加载缓存,在构建时需要实现CacheLoader的load方法。当调用LoadingCache的get方法时,如果缓存不存在对应key的记录,则CacheLoader会依据load方法中返回结果作为value,并从get方法返回。如果load方法中返回空,则抛出异常。

LoadingCache<String,Object> loadingCache = CacheBuilder.newBuilder().build(
                new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String s) throws Exception {
                        if(s.equals("key1")){
                            return "value";
                        }
                       return null;
                    }
                });
System.out.println(loadingCache.get("key1")); //返回load方法中指定的“value”
System.out.println(loadingCache.get("key2")); //不满足load方法的if判断,返回空,抛出ExecutionException

Caffeine

Caffeine是Java8对Guava缓存的重写版本,支持多种缓存过期策略,底层数据存储使用的也是面向JDK8的ConcurrentHashMap(增加了红黑树,在hash冲突时也保持读性能)。Guava Cache 功能虽然强大,但是只是对 LRU 的一层封装,在复杂的业务场景下,LRU 淘汰策略显得力不从心。为此基于 W-TinyLFU(LFU+LRU算法的变种) 淘汰策略的进程内缓存 —— Caffeine Cache 诞生了。也就是说Caffeine与GuavaCache相比,在功能基本一致的情况下,通过对算法和部分逻辑的优化完成了对性能和缓存命中率的提升。

基本用法

构建缓存对象

与GuavaCache基本上没有太大区别,都是通过builder方式进行构建,设置过期方式,刷新时间,统计信息等。但是在使用LoadingCache设置自动加载缓存时,若为空,GuavaCache会抛错,caffeine直接返回null值。

//--自动填充--
LoadingCache<String,Object> cache = Caffeine.newBuilder()     		
                .build(key -> load());

//缓存数据来源
public  String load(){
        return null;
    }

System.out.println(cache.get("key1"));  //直接返回空

//--手动填充--
Cache<String,Integer> cache2 = Caffeine.newBuilder().build();
//先从缓存中查询,1.缓存中能找到,直接返回,2.找不到调用自定义的方法返回        
Integer age2 = cache2.get("张三",k->{
    //在实际业务中这里可以把我们代码中的mapper传入进去,进行数据源的刷新。
       return 18;
});
System.out.println(age2);

//--异步手动填充--
AsyncCache<String, Integer> cache = Caffeine.newBuilder().buildAsync();

//会返回一个 future对象, 调用future对象的get方法会一直卡住直到得到返回,和多线程的submit一样
CompletableFuture<Integer> ageFuture = cache.get("张三", name -> {
      System.out.println("name:" + name);
      return 18;
});
        
Integer age = ageFuture.get();
System.out.println("age:" + age);

缓存清除

Caffeine的缓存清除是惰性的,可能发生在读请求后或者写请求后,比如说有一条数据过期后,不会立即删除,需要下一次读/写操作后触发删除。与GuavaCache一样,Caffeine的缓存清除也有三种:

  • 基于大小

         有两种方式,一种是基于内存的大小,一种是基于权重,基于内存大小的清除设置,设置基于内存大小的清除缓存,会使用maximumSize指定缓存大小,当超过该阀值会使用W-TinyLFU算法来淘汰掉最近很久没有被访问的或不常被使用的数据。示例如下:

//基于设置的内存大小清除缓存
 Cache<String, String> cache5 = Caffeine.newBuilder().maximumSize(1)
                .build();
cache5.put("测试1", "test1");
cache5.put("测试2", "test2");
cache5.cleanUp();
System.out.println(cache5.getIfPresent("测试1"));
System.out.println(cache5.getIfPresent("测试2"));
  • 基于时间

       expireAfterWrite:在最后一次写入缓存后开始计时,在指定时间后过期:

Cache<String,String> cache6 = Caffeine.newBuilder().maximumSize(1).
                expireAfterWrite(2,TimeUnit.MINUTES)
                .build();
        cache6.put("test1","1");
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("test1"));  //还存在值
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("test1")); //缓存过期,返回null
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("test1")); //缓存过期,返回null

最后的结果:在第二次时打印null
  • 基于引用

        expireAfterAccess:在最后一次读或写之前开始计时,在指定时间后若没有没有访问引用到这个值才会过期:

Cache<String, String> cache7 =  Caffeine.newBuilder()
                // 最大容量为1
                .maximumSize(1)
                .expireAfterAccess(2, TimeUnit.SECONDS) //设置2秒后无访问引用过期
                .build();
        cache7.put("1","test1");
        Thread.sleep(1*1000);
        System.out.println(cache7.getIfPresent("1")); //线程等待1秒后访问,返回test1
        Thread.sleep(1*1000);
        System.out.println(cache7.getIfPresent("1"));  //线程等待1秒后访问,返回test1
        Thread.sleep(3001);
        System.out.println(cache7.getIfPresent("1")); //线程等待3秒后超过过期时间且无访问,返回null
  • 显式回收

除了设置过期时间来将时间范围内未被访问的缓存项删除之外,还能通过以下方法将缓存显式删除,功能与guavaCache相同,分成

  • 使用Cache.invalidate(key)单个移除;

  • 使用Cache.invalidteAll(keys)批量移除;

  • 使用Cache.invalidateAll()移除全部。

两种缓存接口

与GuavaCache相同,也是会有Cache和支持自定义加载方式的LoadingCache两种。

更新缓存

refreshAfterWrite:设置创建缓存 或者 最近一次更新缓存后指定时间刷新缓存,在业务项目中因为一些配置数据的变更会对返回的结果有影响所以常用该参数设置 更新配置后刷新缓存的时间。

@PostConstruct
    public void init() {
        sellerIdAndParkCodeCache = Caffeine.newBuilder()
                .refreshAfterWrite(5, TimeUnit.MINUTES) //5分钟更新
                .build(key -> refreshCache());
    }

缓存刷新

缓存刷新有两种方式,refreshAfterWrite和expireAfterWrite。两者都是在每次更新之后的指定时间让缓存失效,然后重新加载缓存。

expireAfterWrite:会严格限制只有1个线程在进行加载操作,旧值会被删除,其他线程会阻塞等待这个线程加载操作完成后,释放锁,然后他们获取锁在拿到更新后的值,能保证数据的准确性但是对性能有损耗。

refreshAfterWrite:会严格限制只有1个线程在进行加载操作异步刷新,旧值不会被删除,其他线程先返回旧值,这样能减少锁等待,但是因为不能保证每一次的加载都获得新值,会造成加载失败后会拿到旧值的问题,不能保证数据的准确性。

结论:对于互联网高并发的场景,refreshAfterWrites这种使用异步刷新缓存的方法应该是我们首先考虑的,取到一个过期的旧值总比大量线程全都被block要好,不然可能会导致请求大量堆积,连接数不够等一些列问题。

分布式缓存

本地缓存是与业务系统耦合在一起,不同应用直接无法共享缓存内容,每个应用需要维护自己的缓存,分布式缓存自身就是一个独立的多个应用可以共享缓存,常见的分布式缓存有redis,MemCached等。

Redis

redis也是一个单进程单线程的KV的数据结构的缓存中间件,用集群部署、主从同步实现读写分离,还可用来做分布式锁,使用setnx获取锁,expire给锁加上过期时间,数据结构如下:

持久机制

    redis为内存数据库,数据保存在内存中,为防止变化和丢失,redis有持久化机制AOF和RDB(默认)。

  • RDB

数据以快照的形式保存,在指定时间间隔内将内存中的数据集快照写入磁盘,在fork子进程执行写入期间,其余线程会阻塞直到RDB完成为止,性能高,但是在子进程持久化时,父进程修改的数据不会被保存。

  • ADF

使用日志记录,redis将收到的每一个写命令使用write函数追加到文件中,保证数据不会丢失,但随着数据量的变大,日志文件也会相应变大。所以一般持久化机制是两者都用,先用RDB快速恢复后用AOF数据补全。

淘汰机制

在 Redis 当中,还是使用的LRU算法,但是对传统的LRU算法做了改进,因为传统的 LRU 算法存在 2 个问题:

  • 需要额外的空间进行存储。

  • 可能存在某些 key 值使用很频繁,但是最近没被使用,从而被 LRU 算法删除。

为了避免以上 2 个问题,Redis 当中对传统的 LRU 算法进行了改造,通过抽样的方式进行删除。配置文件中提供了一个属性 maxmemory_samples 5,默认值就是 5,表示随机抽取 5 个 key 值,然后对这 5 个 key 值按照 LRU 算法进行删除,(也就是随机抽5个值,删除这个5个值中 最近最少 被使用的)所以很明显,key 值越大,删除的准确度越高。对抽样 LRU 算法和传统的 LRU 算法,Redis 官网当中有一个对比图:

  • 浅灰色带是被删除的对象。

  • 灰色带是未被删除的对象。

  • 绿色是添加的对象。

 

 

左上角第一幅图代表传统 LRU 算法,可以看到当抽样数达到 10个(右上角),已经和传统的 LRU 算法非常接近了。

常见用法

Redis官方推荐的面向Java的操作Redis的客户端是Jedis,但spring提供的redisTemplate完成了对JedisApi的封装,使用更加简单,redis支持String、List、Set、Hash、sortedSet五种数据类型,redisTemplate封装了对这五种数据类型的操作。

redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set

 引入依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

 基本服务

@Service
public class RedisServiceImpl implements CacheService {
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    //获取值--String类型
    @Override
    public String getFromString(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    //设置值和对应的过期时间
    @Override
    public void setString(String key, String value, Long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    //删除
    @Override
    public boolean delString(String key) {
        return redisTemplate.delete(key);
    }

    //获取值--Hash类型
    @Override
    public void put(String HKey, String key, Object str) {
        redisTemplate.opsForHash().put(HKey, key, str);
    }
	
    //设置hash类型的值对应的过期时间
    @Override
    public void expire(String HKey, long time) {
        redisTemplate.expire(HKey, time, TimeUnit.SECONDS);
    }
	//获取k为传入的key对应的值
    @Override
    public List<Object> values(String key) {
        return redisTemplate.opsForHash().values(key);
    }
    
    //获取变量中的指定map键是否有值,如果存在该map键则获取值,没有则返回null。
     @Override
    public Object get(String Hkey, String key) {
        return redisTemplate.opsForHash().get(Hkey, key);
    }
	
    //设置值,如果不存在就设置
    @Override
    public boolean setKeyIfNotExist(String key, long timeSeconds) {
        return redisTemplate.opsForValue().setIfAbsent(key, key, timeSeconds, TimeUnit.SECONDS);
    }

    //set if not exists ,k不存在则新增传入的kv(且setnx返回1),存在就不会做操作(返回0),长用作分布式锁
    /*如果 SETNX 返回1,说明该进程获得锁,SETNX将time的值设置为锁的超时时间(当前时间 + 锁的有效时间)。
    如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁*/
    @Override
    public Boolean setNX(String key, String value, Duration time) {
        try {
            return redisTemplate.opsForValue().setIfAbsent(key, value, time);
        } catch (Exception ex) {
            return false;
        }
    }

	//解锁,操作完后使用delete方式删除key
    @Override
    public void unlock(String key, String value) {
        String currentValue = redisTemplate.opsForValue().get(key);
        if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
            redisTemplate.opsForValue().getOperations().delete(key);
        }
    }

  

   

双写一致性

从理论上来说,给缓存设置过期时间,是保证数据库与缓存最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

 

当未设置过期时间时,常用以下三种:

  • 先更新数据库,再更新缓存;

  • 先删除缓存,再更新数据库;

  • 先更新数据库,再删除缓存。

 

常见问题

缓存击穿

指大并发下操作热点数据,该数据在缓存中无法查询(一般是缓存时间到),导致大并发量的请求直接进入DB,造成数据库巨大的压力。解决方案如下:

  • 热点数据永不过期,缺点:无法识别热点数据且浪费存储空间,不推荐;

  • 互斥锁,使用redis分布式锁,只有一个线程去查DB,其余线程阻塞等待,等当前线程查到数据并放入缓存后,其余线程都可以从缓存中取。缺点:只能针对于同一个key的情况。

缓存雪崩

缓存整体出现错误,大批量缓存数据过期不能正常工作,所有请求都到DB中,导致服务挂掉。解决方案如下:

  • 使用redis的哨兵集群模式实现redis集群的高可用,监听主从服务器的状态,当master宕机后自动将slave切换成master,发布订阅通知其他从服务器;或采用多机房部署,将热点数据分布在不同的缓存数据库中;

  • 过期时间加上一个随机值,防止过期时间集中;

  • 采用多级缓存;

缓存穿透

上游不断的发起请求在缓存和DB中都查不到的数据,造成数据库压力,例如一些恶意攻击,在一些订单报表页面,伪造一些不存在的订单号然后并发请求页面的查询接口,这些数据在缓存和数据库都查不到,容易造成服务超时甚至宕机。解决方案如下:

  • 恶意输入在前端或请求查询前提前拦截,如输入订单号、id这些肯定不会为负数的字段,添加<=0的条件提前过滤

  • 通过布隆过滤器。查询缓存之前先去布隆过滤器查询下这个数据是否存在。如果数据不存在,然后直接返回空。这样的话也会减少底层系统的查询压力;

 

总结

对于不同的业务场景,缓存有各自不同的用法。具体还需要结合业务场景,毕竟只有具体的业务,没有通用的方法。

 

文|帕琪

关注得物技术,携手走向技术的云端

相关推荐

从中心调度到自驱协作多 Agent 架构的三次反转|得物技术

本文探讨多Agent系统中的控制权分配问题,提出三大协作架构:Master Agent(中心调度)、任务看板(共享状态)与Advisor(轻量咨询)。通过划清上下文边界与模型边界,强调应以信息相关性而非职业标签划分Agent。三种模式分别将决策权集中于中心、下放至共享状态、再下沉至执行者,形成控制权逐步下沉的演进路径。案例显示,合理设计可提升复杂任务的并行性、弹性与可维护性。

SmartCodeTech的博客 397

Jev 实战:为AI工作流引入轻量决策层 x10倍提速|得物技术

完成这三个场景后,我对适用边界有了更明确的认识:它们都可以先定义答案范围,再根据判断结果驱动下一步操作。接入时,我会优先明确输入、选项、采纳条件和兜底路径,再评估Jev是否适合承担这一环节。结合这些实践,我更容易理解Jev(TypeSafe System One)作为专用结构化决策模型不生成自然语言,而是返回带概率的判断结果。后续接入时,我会继续从脚本中边界明确的判断任务开始,分别验证准确性、耗时和费用,再决定是否扩大使用范围。下面整理了相关能力、限制和接入要点,便于查阅。

SmartCodeTech的博客 342

国产算力·昇腾 910C 大模型推理优化(下):源码修改与算子优化,提速30%|得物技术

本文延续上篇,深入vllm-ascend源码优化:通过多流重叠实现MoE共享专家并行计算,消除冗余事件开销,并解除与FUSED_MC2的互斥限制,实现c=1下TPOT从8.2ms降至8.0ms;优化decode阶段host侧开销,引入状态跟踪跳过冗余拷贝;设计Block Table CPU快路径,规避小batch下NPU kernel启动延迟;针对GDN层融合L2Norm至recurrent kernel,减少算子调用与内存往返。综合优化后推理性能显著提升,形成可复用的昇腾910C大模型推理优化方法论。

SmartCodeTech的博客 228

国产算力·昇腾 910C 大模型推理优化(上):镜像更新与参数调优,提速 40%|得物技术

文中详细给出每一项优化的动机、实现(含具体源码改动路径与关键代码片段)、验证数据与踩坑记录(包括十余项实测无效或负向的尝试),希望为同样在国产算力上做推理优化的团队提供一份可直接复用的工程参考。

SmartCodeTech的博客 214

动态规则的 Agent 实践:用确定性工程驾驭 LLM 的资产智能验收

面对财务验收中规则多变、材料非结构化、核对复杂等痛点,传统规则引擎难以应对。系统采用“规则即配置”思路,由大语言模型(LLM)负责理解合同、扫描件等非结构化材料并执行语义比对,工程系统则保障流程编排与可追溯性。通过异步处理、幂等设计、三类流量隔离及双通道(文本/图像)处理,实现高效稳定运行。结合用户反馈构建自动标准答案,辅以试跑、回测与发布门禁机制,形成闭环质量体系。底层采用确定性工作流架构,确保行为可预期、可测试,真正实现灵活规则的快速迭代与安全上线。

SmartCodeTech的博客 342

发一张图,它居然“跃“出屏幕了?一文读懂得物 3D 空间图片技术实践

3D 空间图片功能的落地,涉及模型生成、内容分发、客户端渲染之间的一系列取舍。本文重点分享如何生产出高质量、小体积的模型,以及如何在手机上流畅运行。

SmartCodeTech的博客 555

别再只卷向量检索了,得物交易搜索如何用“生成式”实现召回范式跃迁?

你搜"af1",它得给你"空军一号"。这一年,得物交易搜索把召回从"做选择题"改成了"写答案"——不再从商品池里"找"最像的,而是让模型自己"生成"你真正想要的。从 LLM 语义索引到 HLLM、语义 ID,搜索是怎么从"检索"走向"生成"的?

SmartCodeTech的博客 593

指标平台:从语义底座到智能消费的实践路径|得物技术

这篇文章想做的,不是在本体化的宏大叙事上再叠加一层理论,而是把我们在真实落地中验证过的路径讲清楚:语义底座怎么从零建立起来、怎么保证质量不失控、怎么支撑 Text2SQL 和 AI Coding 两条消费主线,以及沿着这条路径往前走,下一步该往哪个方向发力。

SmartCodeTech的博客 314

企业级 MultiAgent 落地:Plan 模式与主子 Agent 协作|得物技术

基于 AgentScope Java 的企业级 MultiAgent 平台,为研发、数据分析和知识问答等场景提供统一的模型调用、工具接入、任务编排、流式输出与运行追踪能力。

SmartCodeTech的博客 299

得物小摊 AI Native 演进实录:用 Harness 构建可控 AI 交付

AI Coding 一个人用很顺手,进了团队就漏:指令没人审、经验难沉淀、过程看不见、链路不闭环。作者团队基于 Multica 做了 EP-Harness,把 Agent 当成员管,给它套一层可执行、可观察、可治理的 Harness,并让反馈闭环。成果:可追溯决策、多 Agent 协作,高频异常从每 4 小时 2400 多条降到个位数,对得物技术补上团队级 AI 协作关键一环。

SmartCodeTech的博客 539

企业级 MultiAgent 的记忆系统:短期上下文与四层记忆架构实现|得物技术

在 MultiAgent 平台中,Agent 的"记忆"能力是实现智能对话和个性化服务的核心资产。本文深入剖析 Agent 记忆系统的设计理念与实现细节,重点解析长期记忆架构的写入与加载流程,涵盖 LLM 智能判断、语义检索、Token 预算控制、冲突处理等关键技术,为开发者提供完整的技术实现参考。

SmartCodeTech的博客 502

EP-Harness:从个人 AI Coding 到团队级 Agent 工作流|得物技术

AI Coding 个人用得爽,一到团队就崩。EP-Harness 把 Agent 从聊天工具变成团队协作成员——可管理、可追溯、可闭环。落地后高频异常从 2400+/4h 收敛到个位数。

SmartCodeTech的博客 688

得物知识问答:复合检索 Agent 的系统设计实践

从"搜到什么用什么"到"搜→筛→挖→验"自主决策闭环——我们造了一个有判断力的 RAG Agent,不是套壳。

SmartCodeTech的博客 732

实战从零开始构建一个Coding Agent:Violin |得物技术

理解了 coding agent 的构建原理,也就掌握了理解其他 agent 的一把钥匙。笔者对每天都在使用的 agent 原理很感兴趣,于是决定自己写一个:violin。

SmartCodeTech的博客 926

AI Native 交易核心系统的研发范式|得物技术

如何改造旧研发流程适配 AI 编码?本文提出五道标准化关口治理思路,依托 Claude Code Spec-Driven 插件打通需求澄清、方案设计、TDD 落地、门禁管控与埋点监控,构建带人工确认门控的完整工作流,以五层自底向上架构实现 AI 编码规范化落地。

SmartCodeTech的博客 971

推荐系统体验的数字化突破:得物自动化评测平台的技术实践|AICon 文章整理

「得物推荐 AI Harness 工程化实践系列」的末篇内容,本系列共三篇连载。本篇(末篇)详解得物推荐评测平台的完整技术体系:从主观体验量化到大模型自动化评测流水线,涵盖多维度指标设计、人机协作提示词校验闭环、分布式任务调度与成本精控等核心工程实现,以及单周百万级审计需求下实现 91% 资源成本优化的工业级落地实战细节。

SmartCodeTech的博客 1492

RAG 核心概念与原理:Chunking、Embedding、相似度、HNSW 与多路召回|得物技术

决定 RAG 效果的关键不在大模型,而是文本切块、查询改写、多路召回与精排。合理切块存好知识库内容,向量匹配抓语义、BM25 匹配关键词互补。先优化用户提问,快速召回一批相关文档,再用精排模型精细筛选,整套检索流程配合到位,问答准确度才会明显提升。

SmartCodeTech的博客 1079

从“机械应答“到“服务伙伴“:得物高可控智能客服的 Agent 工程实践|AICon 演讲整理

本次分享围绕以下核心技术展开:客服 Agent 挑战与架构设计(Single-Agent→Multi-Agent→Harness);高可控性数据飞轮(PE 自动化 + DPO 训练);RL 策略训练和人工经验对齐;多轮半双工消息流控制。

SmartCodeTech的博客 743

得物推荐系统诊断 Agent:从 “调接口” 到 “会思考”|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。AICon上海2026_全球人工智能开发与应用大会暨大模型应用生态展_InfoQ技术大会「得物推荐 AI Harness 工程化实践系列」的中篇内容,本系列共三篇连载。本篇(中篇)进一步详解得物推荐复杂业务场景下,AI代码从生成、防护校验到安全上线的完整技术体系、自研AI Harness整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。

SmartCodeTech的博客 1361

得物 OceanBase 落地实践

得物选型OceanBase多模数据库应对业务挑战,通过验证其高可用架构、存储压缩技术及混合负载处理能力。测试显示OceanBase在复杂查询场景性能较MySQL提升130-200倍,在高并发TP/AP混合场景表现优异,存储成本降低40%+。通过DBA内部报表业务试点验证其稳定性后,逐步推广至核心业务场景,优化数据库技术栈,实现性能与成本的双重收益。

SmartCodeTech的博客 1380

AI UITester:AI Native 的 UI 自动化测试新范式|得物技术

ai_uitester 代表了 AI Native 的 UI 自动化测试新范式:这不仅是工具的升级,而是测试范式的转变 ------ 从 "代码驱动" 转向 "视觉驱动",从 "人工调试" 转向 "AI 自愈",从 "三端分离" 转向 "统一抽象"。

SmartCodeTech的博客 774

从狂野代码到按目标生产:得物推荐 AI Harness 的工程化实践|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。「得物推荐 AI Harness 工程化实践系列」的开篇内容,本系列共三篇连载。将系统拆解得物推荐复杂业务场景下,AI 代码从生成、防护校验到安全上线的完整技术体系,逐一详解自研 AI Harness 整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。本篇(上篇):将整体介绍团队自研的 AI Harness 体系,包括建设思路、全生命周期防护机制、混合智能体架构及落地效果。

SmartCodeTech的博客 1079

从表单到 Agent:得物社区活动搭建的 AI 实践之路

一场营销活动从策划到上线,运营要在三个系统间跳转 10 + 次、填写 40 + 个字段。我们用 AI 重新设计了这条链路 —— 从 “AI 帮你填表单” 到 “两阶段 Agent + 聚合工作台”。这篇文章记录的不是技术细节,而是这条路上的选择和反思。

SmartCodeTech的博客 740

从埋点需求到规则资产:Hermes Agent 重构得物数仓工作流

埋点和指标需求中,消耗数据承接方的不是生成答案,而是补齐证据链、对齐历史口径、识别下游影响,并在生产写入前留下可追溯的确认记录。本文数据承接方指承接业务需求的数据同学或业务合作伙伴;后文讨论内部研发、元数据、查询和门禁能力在流程中的角色。

SmartCodeTech的博客 655

让 Claude Code 拥有自我进化和记忆系统|得物技术

本文记录了为 Claude Code 构建的一套持久化记忆与自我学习系统的设计思路与实现细节。这套系统让 AI 助手能够跨越会话边界记住用户习惯,并在每次对话结束后自动提炼行为规律和系统信息,通过向量检索快速提取关键信息,下一次对话时主动应用,逐步进化为更懂你的编程伙伴。

SmartCodeTech的博客 993

用 LLM Agent 重构告警排查流程|得物技术

告警排查需频繁切换日志、APM、链路追踪等多个平台,耗时 10~30 分钟,且高度依赖个人经验。我们做了 Troubleshooter——用 LLM Agent 自动完成数据采集、根因分析和处置建议生成。上线后中位数排查耗时从 20 分钟降到 4.4 分钟,覆盖 11 个服务和 10+ 种告警类型。

SmartCodeTech的博客 1002

HorizonVault 技术深潜:如何在 HDD 上做出 100GB/s+ 级大吞吐分布式存储|得物技术

HorizonVault是一款面向Kafka远程存储的高吞吐分布式存储引擎,通过分布式并行写入和状态感知调度解决冷数据存储的三大核心问题:吞吐瓶颈、性能抖动和恢复能力。系统采用分层架构,Meta负责全局资源调度,Broker处理请求路由,Store实现本地磁盘管理,形成完整的写入治理链路。通过将HDD磁盘提升为一等资源,实现顺序追加写入、多盘并行和慢盘隔离,在保证高吞吐的同时控制延迟。存储结构采用DiskStore组织磁盘资源,LogSegment顺序写入数据,IndexSegment定位读取,Change

SmartCodeTech的博客 889

Claude Code Harness 工程:数仓侧落地方案|得物技术

Harness 工程通过职责分层实现研发流水线可靠性提升。Claude 负责需求理解与代码生成等语义层工作;hooks 负责规范检查等确定性执行;Subagents 隔离血缘查询、自测等高 token 操作;Memory 承载跨会话状态持久化。四层协同使数仓 AI 开发从"对话式辅助"转变为"规则驱动的自动化流程"。

SmartCodeTech的博客 1090

BP Claw 破解 AI 编码输入难题 ——FlinkSpec 需求智能化实践|得物技术

本文为 FlinkSpec 系列之开篇,亦是这场工程化变革的序章。BP Claw 所立之处,不过链路之源。FlinkSpec 所图,乃以 AI 之力,将实时数仓从需求落地至验收上线的全程工序,熔铸为一套精密自洽、生生不息的智能工程体系。宏图未竟,后续系列将逐章揭幕,敬请期待!

SmartCodeTech的博客 1034

基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践|得物技术

Harness(约束)思维的本质是:给 AI 一个已有的实现作为参照,让它照着复刻一份,而不是凭空创造。就像给一个新入职的工程师说 "你照着这个模块的风格,写一个类似的",而不是 "你自由发挥"------ 前者往往能更快产出符合团队规范的代码。

SmartCodeTech的博客 1181
上一篇: 【得物技术】软件工程与PlantUML实战
下一篇: 【得物技术】统一OLAP查询平台之语义层
得物技术
得物技术 得物技术官方账号 得物技术官方账号
博客等级 码龄6年 2492粉丝 407原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值