2024年2月

分享是最有效的学习方式。

博客:
https://blog.ktdaddy.com/

老猫的设计模式专栏已经偷偷发车了。不甘愿做crud boy?看了好几遍的设计模式还记不住?那就不要刻意记了,跟上老猫的步伐,在一个个有趣的职场故事中领悟设计模式的精髓。还等什么?赶紧上车吧

如果把系统软件比喻成江湖的话,那么设计原则绝对是OO程序员的武功心法,而设计模式绝对是招式。光知道心法是没有用的,还是得配合招式。只有心法招式合二为一,遇到强敌(“坑爹系统”)才能见招拆招,百战百胜。

故事

之前让小猫梳理的业务流程以及代码流程基本已经梳理完毕【
系统梳理大法
&
代码梳理大法
】。从代码侧而言也搞清楚了系统臃肿的原因【
违背设计原则
】。小猫逐渐步入正轨,他决定从一些简单的业务场景入手,开始着手优化系统代码。那么什么样的业务代码,动了之后影响最小呢?小猫看了看,打算就从泛滥创建的线程池着手吧,他打算用单例模式做一次重构。

在小猫接手的系统中,线程池的创建基本是想在哪个类用多线程就在那个类中直接创建。所以基本上很多service服务类中都有创建线程池的影子。

写在前面

遇到上述小猫的这种情况,我们的思路是采用单例模式进行提取公共线程池执行器,然后根据不同的业务类型使用工厂模式进行分类管理。

接下来,我们就单例模式开始吧。

概要

单例模式定义

单例模式(Singleton)又叫单态模式,它出现目的是为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。从这点可以看出,单例模式的出现是为了可以保证系统中一个类只有一个实例而且该实例又易于外界访问,从而方便对实例个数的控制并节约系统资源而出现的解决方案。
如下图:

单例模式简单示意图

饿汉式单例模式

什么叫做饿汉式单例?为了方便记忆,老猫是这么理解的,饿汉给人的形象就是有食物就迫不及待地去吃的形象。那么饿汉式单例模式的形象也就是当类创建的时候就迫不及待地去创建单例对象,这种单例模式是绝对线程安全的,因为这种模式在尚未产生线程之前就已经创建了单例。

看一下示例,如下:

/**
 * 公众号:程序员老猫
 * 饿汉单例模式 
 */
public class HungrySingleton {

    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    //构造函数私有化,保证不被new方式多次创建新对象
    private HungrySingleton() {
    }

    public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
    }
}

我们看一下上述案例的优缺点:

  • 优点:线程安全,类加载时完成初始化,获取对象的速度较快。
  • 缺点:由于类加载的时候就完成了对象的创建,有的时候我们无需调用的情况下,对象已经存在,这样的话就会造成内存浪费。

当前硬件和服务器的发展,快于软件的发展,另外的,微服务和集群化部署,大大降低了横向扩展的门槛和成本,所以老猫觉得当前的内存其实是不值钱的,所以上述这种单例模式硬说其缺点有多严重其实也不然,个人觉得这种模式用于实际开发过程中其实是没有问题的。

其实在我们日常使用的spring框架中,IOC容器本身就是一个饿汉式单例模式,spring启动的时候就将对象加载到了内存中,这里咱们不做展开,等到后续咱们梳理到spring源代码的时候再展开来说。

懒汉式单例模式

上述饿汉单例模式我们说它的缺点是浪费内存,因为其在类加载的时候就创建了对象,那么针对这种内存浪费的解决方案,我们就有了“懒汉模式”。对于这种类型的单例模式,老猫是这么理解的,懒汉的定义给人的直观感觉是懒惰、拖延。那么对应的模式上来说,这种方案创建对象的方法也是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象,否则则先执行实例化操作。

看一下示例,如下:

/**
 * 公众号:程序员老猫
 * 懒汉式单例模式
 */
public class LazySingleton {
    private LazySingleton() {
    }

    private static LazySingleton lazySingleton = null;
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
              lazySingleton =  new LazySingleton();
        }
        return lazySingleton;
    }
}

上面这种单例模式创建对象,内存问题看起来是已经解决了,但是这种创建方式真的就线程安全了么?咱们接下来写个简单的测试demo:

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            LazySingleton lazySingleton = LazySingleton.getInstance();
            System.out.println(lazySingleton.toString());
        });
        Thread thread2 = new Thread(()->{
            LazySingleton lazySingleton = LazySingleton.getInstance();
            System.out.println(lazySingleton.toString());
        });
        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

执行输出结果如下:

end
LazySingleton@3fde6a42
LazySingleton@2648fc3a

从上述的输出中我们很容易地发现,两个线程中所获取的对象是不同的,当然这个是有一定概率性质的。所以在这种多线程请求的场景下,就出现了线程安全性问题。

聊到共享变量访问线程安全性的问题,我们往往就想到了锁,所以,咱们在原有的代码块上加上锁对其优化试试,我们首先想到的是给方法代码块加上锁。

加锁后代码如下:

public class LazySingleton {

    private LazySingleton() {
    }

    private static LazySingleton lazySingleton = null;
    public synchronized static LazySingleton getInstance() {
        if (lazySingleton == null) {
              lazySingleton =  new LazySingleton();
        }
        return lazySingleton;
    }
}

经过上述同样的测试类运行之后,我们发现问题似乎解决了,每次运行之后得到的结果,两个线程对象的输出都是一致的。

我们用线程debug的方式看一下具体的运行情况,如下图:

线程输出

我们可以发现,当一个线程进行初始化实例时,另一个线程就会从Running状态自动变成了Monitor状态。试想一下,如果有大量的线程同时访问的时候,在这样一个锁的争夺过程中就会有很多的线程被挂起为Monitor状态。CPU压力随着线程数的增加而持续增加,显然这种实现对性能还是很有影响的。

那还有优化的空间么?当然有,那就是大家经常听到的“DCL”即“Double Check Lock”
实现如下:

/**
 * 公众号:程序员老猫
 * 懒汉式单例模式(DCL)
 * Double Check Lock
 */
public class LazySingleton {

    private LazySingleton() {
    }
    //使用volatile防止指令重排
    private volatile static LazySingleton lazySingleton = null;
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            synchronized (LazySingleton.class) {
                if(lazySingleton == null){
                    lazySingleton =  new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
}

通过DEBUG,我们来看一下下图:

双重校验锁

这里引申一个常见的问题,大家在面试的时候估计也会碰到。
问题:为什么要double check?去掉第二次check行不行?

回答:当2个线程同时执行getInstance方法时,都会执行第一个if判断,由于锁机制的存在,会有一个线程先进入同步语句,而另一个线程等待,当第一个线程执行了new Singleton()之后,就会退出synchronized的保护区域,这时如果没有第二重if判断,那么第二个线程也会创建一个实例,这就破坏了单例。

问题:这里为什么要加上volatile修饰关键字?
回答:这里加上该关键字主要是为了防止"指令重排"。关于“指令重排”具体产生的原因我们这里不做细究,有兴趣的小伙伴可以自己去研究一下,我们这里只是去分析一下,“指令重排”所带来的影响。

lazySingleton =  new LazySingleton();

这样一个看似简单的动作,其实从JVM层来看并不是一个原子性的行为,这里其实发生了三件事。

  1. 给LazySingleton分配内存空间。
  2. 调用LazySingleton的构造函数,初始化成员字段。
  3. 将LazySingleton指向分配的内存空间(注意此时的LazySingleton就不是null了)

在此期间存在着指令重排序的优化,第2、3步的顺序是不能保证的,最后的执行顺序可能是1-2-3,也可能是1-3-2,假如执行顺序是1-3-2,我们看看会出现什么问题。看一下下图:

指令重排执行

从上图中我们看到虽然LazySingleton不是null,但是指向的空间并没有初始化,最终被业务使用的时候还是会报错,这就是DCL失效的问题,这种问题难以跟踪难以重现可能会隐藏很久。

JDK1.5之前JMM(Java Memory Model,即Java内存模型)中的Cache、寄存器到主存的回写规定,上面第二第三的顺序无法保证。JDK1.5之后,SUN官方调整了JVM,具体化了volatile关键字,private volatile static LazySingleton lazySingleton;只要加上volatile,就可以保证每次从主存中读取(这涉及到CPU缓存一致性问题,感兴趣的小伙伴可以研究研究),也可以防止指令重排序的发生,避免拿到未完成初始化的对象。

上面这种方式可以有效降低锁的竞争,锁不会将整个方法全部锁定,而是锁定了某个代码块。其实完全做完调试之后我们还是会发现锁争夺的问题并没有完全解决,用到了锁肯定会对整个代码的执行效率带来一定的影响。所以是否存在保证线程的安全,并且能够不浪费内存完美的解决方案呢?一起看下下面的解决方案。

内部静态类单例模式

这种方式其实是利用了静态对象创建的特性来解决上述内存浪费以及线程不安全的问题。
在这里我们要弄清楚,被static修饰的属性,类加载的时候,基本属性就已经加载完毕,但是静态方法却不会加载的时候自动执行,而是等到被调用之后才会执行。并且被STATIC修饰的变量JVM只为静态分配一次内存。(这里老猫不展开去聊static相关知识点,有兴趣的小伙伴也可以自行去了解一下更多JAVA中static关键字修饰之后的类、属性、方法的加载机制以及存储机制)

所以综合这一特性,我们就有了下面这样的写法:

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton () {
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

上面这种写法,其实也属于“懒汉式单例模式”,并且这种模式相对于“无脑加锁”以及“DCL”以及“饿汉式单例模式”来说无疑是最优的一种实现方式。

但是深度去追究的话,其实这种方式也会有问题,这种写法并不能防止反序列化和反射生成多个实例。
我们简单看一下反射的破坏的测试类:

public class DestructionSingletonTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<LazyInnerClassSingleton> enumSingletonClass = LazyInnerClassSingleton.class;
        //枚举默认有个String 和 int 类型的构造器
        Constructor constructor = enumSingletonClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        //利用反射调用构造方法两次直接创建两个对象,直接破坏单例模式
        LazyInnerClassSingleton singleton1 = (LazyInnerClassSingleton) constructor.newInstance();
        LazyInnerClassSingleton singleton2 = (LazyInnerClassSingleton) constructor.newInstance();
    }
}

这里序列化反序列化单例模式破坏老猫偷个懒,因为下面会有写到,有兴趣的小伙伴继续看下文,老猫觉得这种破坏场景在真实的业务使用场景比较极端,如果不涉及底层框架变动,光从业务角度来看,上面这些单例模式的实现已经管够了。
当然如果硬是要防止上面的反射创建单例两次问题也能解决,如下:

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton () {
        if(LazyHolder.LAZY != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

写到这里,可能大家都很疑惑了,咋还没提及用单例模式优化线程池创建。下面这不来了么,老猫个人觉得上面的这种方式进行创建单例还是比较好的,所以就用这种方式重构一下线程池的创建,具体代码如下:

public class InnerClassLazyThreadPoolHelper {
    public static void execute(Runnable runnable) {
        ThreadPoolExecutor threadPoolExecutor = ThreadPoolHelperHolder.THREAD_POOL_EXECUTOR;
        threadPoolExecutor.execute(runnable);
    }
    /**
     * 静态内部类创建实例(单例).
     * 优点:被调用时才会创建一次实例
     */
    public static class ThreadPoolHelperHolder {
        private static final int CPU = Runtime.getRuntime().availableProcessors();
        private static final int CORE_POOL_SIZE = CPU + 1;
        private static final int MAXIMUM_POOL_SIZE = CPU * 2 + 1;
        private static final long KEEP_ALIVE_TIME = 1L;
        private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
        private static final int MAX_QUEUE_NUM = 1024;

        private ThreadPoolHelperHolder() {
        }

        private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TIME_UNIT,
                new LinkedBlockingQueue<>(MAX_QUEUE_NUM),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

到此就结束了吗?当然不是,我们之前说上面这种单例创建模式的弊端是可以被反射或者序列化给攻克,虽然这种还是比较少的,但是技术么,还是稍微钻一下牛角尖。有没有一种单例模式不惧反射以及单例模式呢?显然是有的。我们看下被很多人认为完美单例模式的枚举类的写法。

枚举式单例模式

public enum EnumSingleton {
    INSTANCE;
    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

上面我们写过反射模式破坏“静态内部类单例模式”,那么这里咱们补一下序列化反序列化的例子。具体如下:

public class EnumSingletonTest {
    public static void main(String[] args) {
        try {
            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setObject(new Object());

            FileOutputStream fileOutputStream = new FileOutputStream("EnumSingletonTest");
            ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();

            FileInputStream fileInputStream = new FileInputStream("EnumSingletonTest");
            ObjectInputStream ois = new ObjectInputStream(fileInputStream);
            EnumSingleton instance1  = (EnumSingleton) ois.readObject();
            ois.close();
            System.out.println(instance2.getObject());
            System.out.println(instance1.getObject());
        }catch (Exception e) {
        }
    }
}

最终我们发现其输出的结果是一致的。大家可以参考老猫的代码自己写一下测试,关于反射破坏的方式老猫就不展开了,因为上面已经有写法了,大家可以参考一下,自行做一下测试。

那么既然枚举类的单例模式这么完美,我们就拿它来重构线程池的获取吧。
具体代码如下:

public enum EnumThreadPoolHelper {
    INSTANCE;

    private static final ThreadPoolExecutor executor;

    static {
        final int CPU = Runtime.getRuntime().availableProcessors();
        final int CORE_POOL_SIZE = CPU + 1;
        final int MAXIMUM_POOL_SIZE = CPU * 2 + 1;
        final long KEEP_ALIVE_TIME = 1L;
        final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
        final int MAX_QUEUE_NUM = 1024;
        executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TIME_UNIT,
                new LinkedBlockingQueue<>(MAX_QUEUE_NUM),
                new ThreadPoolExecutor.AbortPolicy());
    }

    public void execute(Runnable runnable) {
        executor.execute(runnable);
    }

}

当然在上述中,针对赋值的方式老猫用了static代码块自动类加载的时候就创建好了对象,大家也可以做一下其他优化。不过还是得要保证单例模式。判断是否为单例模式,老猫这里有个比较粗糙的办法。我们打印出成员对象变量的值,通过多次调用看看其值是否一样即可。当然如果大家还有其他好办法也欢迎留言。

总结

针对单例模式相信大家对其有了一个不错的认识了。在日常开发的过程中,其实我们都接触过,spring框架中,IOC容器本身就是单例模式的,当然上述老猫也有提及到。框架中的单例模式,咱们等全部梳理完毕设计模式之后再去做深入探讨。

关于单例模式的优点也是显而易见的:

  1. 提供了对惟一实例的受控访问。
  2. 因为在系统内存中只存在一个对象,所以能够节约系统资源,对于一些须要频繁建立和销毁的对象单例模式无疑能够提升系统的性能。

那么缺点呢?大家有想过么?我们就拿上面的线程池创建这个例子来说事儿。我们整个业务系统其实有很多类别的线程池,如果说我们根据不同的业务类型去做线程池创建的拆分的话,咱们是不是需要写很多个这样的单例模式。那么对于实际的开发过程中肯定是不友好的。
所以主要缺点可想而知。

  1. 因为单利模式中没有抽象层,所以单例类的扩展有很大的困难。
  2. 从开发者角度来说,使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用new关键字实例化对象。

所以具体场景还得具体分析。

上面老猫聊到了不同业务调用创建不同业务线程池的问题,可能需要定义不同的threadFactory名称,那么此时,我们该如何去做?带着疑问,让我们期待接下来的其他模式吧。

前言

OOM 几乎是笔者工作中遇到的线上 bug 中最常见的,一旦平时正常的页面在线上出现页面崩溃或者服务无法调用,查看服务器日志后你很可能会看到“
Caused by: java.lang.OutOfMlemoryError: Java heap space
” 这样的提示,那么毫无疑问表示的是 Java 堆内存溢出了。

其中又当属集合内存溢出最为常见。
你是否有过把整个数据库表查出来的全字段结果直接赋值给一个 List 对象?是否把未经过过滤处理的数据赋值给 Set 对象进行去重操作?又或者是在高并发的场景下创建大量的集合对象未释放导致 JVM 无法自动回收?

Java 堆内存溢出

我的解决方案的核心思路有两个:一是从代码入手进行优化;二是从硬件层面对机器做合理配置。


一、代码优化

下面先说从代码入手怎么解决。

1.1Stream 流自分页

/**
 * 以下示例方法都在这个实现类里,包括类的继承和实现
 */
@Service
public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService{}

在循环里使用
Stream
流的
skip()+limit()
来实现自分页,直至取出所有数据,不满足条件时终止循环

    /**
     * 避免集合内存溢出方法(一)
     * @return
     */
    private List<StudyVO> getList(){
        ArrayList<StudyVO> resultList = new ArrayList<>();
        //1、数据库取出源数据,注意只拿 id 字段,不至于溢出
        List<String> idsList = this.list(new LambdaQueryWrapper<Study>()
                                        .select(Study::getId)).stream()
                                        .map(Study::getId)
                                        .collect(Collectors.toList());
        //2、初始化循环
        boolean loop = true;
        long number = 0;
        long perSize = 5000;
        while (loop){
            //3、skip()+limit()组合,限制每次只取固定数量的 id
            List<String> ids = idsList.stream()
                                      .skip(number * perSize)
                                      .limit(perSize)
                                      .collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(ids)){
                //根据第3步的 id 去拿数据库的全字段数据,这样也不至于溢出,因为一次只是 5000 条
                List<StudyVO> voList = this.listByIds(ids).stream()
                        .map(e -> e.copyProperties(StudyVO.class))
                        .collect(Collectors.toList());
                //addAll() 方法也比较关键,快速地批量添加元素,容量是比较大的
                resultList.addAll(voList);
            }
            //4、判断是否跳出循环
            number++;
            loop = ids.size() == perSize;
        }
        return resultList;
    }

1.2数据库分页

这里是用数据库语句查询符合条件的指定条数,循环查出所有数据,不满足条件就跳出循环

    /**
     * 避免集合内存溢出方法(二)
     * @param param
     * @return
     */
    private List<StudyVO> getList(String param){
        ArrayList<StudyVO> resultList = new ArrayList<>();
        //1、构造查询条件
        String id = "";
        //2、初始化循环
        boolean loop = true;
        int perSize = 5000;
        while (loop){
            //分页,固定每次循环都查 5000 条
            Page<Study> studyPage = this.page(new Page<>
                                    (NumberUtils.INTEGER_ZERO, perSize), 
                                     wrapperBuilder(param, id));
            if (Objects.nonNull(studyPage)){
                List<Study> studyList = studyPage.getRecords();
                if (CollectionUtils.isNotEmpty(studyList)){
                    //3、每次截取固定数量的标识,数组下标减一
                    id = studyList.get(perSize - NumberUtils.INTEGER_ONE).getId();
                    //4、判断是否跳出循环
                    loop = studyList.size() == perSize;
                    //添加进返回的 VO 集合中
                    resultList.addAll(studyList.stream()
                                      .map(e -> e.copyProperties(StudyVO.class))
                                      .collect(Collectors.toList()));
                }
                else {
                    loop = false;
                }
            }
        }
        return resultList;
    }

    /**
     * 条件构造
     * @param param
     * @param id
     * @return
     */
    private LambdaQueryWrapper<Study> wrapperBuilder(String param, String id){
        LambdaQueryWrapper<Study> wrapper = new LambdaQueryWrapper<>();
        //只查部分字段,按照 id 的降序排列,形成顺序
        wrapper.select(Study::getUserAvatar)
                .eq(Study::getOpenId, param)
                .orderByAsc(Study::getId);
        if (StringUtils.isNotBlank(id)){
            //这步很关键,只查比该 id 值大的数据
            wrapper.gt(Study::getId, id);
        }
        return wrapper;
    }

1.3其它思考

以上从根本上还是解决不了内存里处理大量数据的问题,取出 50w 数据放内存的风险就很大了。
以下是我的其它解决思路:

  • 从业务上拆解:明确什么情况下需要后端处理这么多数据?是否可以考虑在业务流程上进行拆解?或者用其它形式的页面交互代替?
  • 数据库设计:数据一般都来源于数据库,库/表设计的时候尽量将表与表之间解耦,表字段的颗粒度放细,即多表少字段,查询时只拿需要的字段;
  • 数据放在磁盘:比如放到 MQ 里存储,然后取出的时候注意按固定数量批次取,并且注意释放资源;
  • 异步批处理:如果业务对实时性要求不高的话,可以异步批量把数据添加到文件流里,再存入到 OSS 中,按需取用;
  • 定时任务处理:询问产品经理该功能或者实现是否是结果必须的?是否一定要同步处理?可以考虑在一个时间段内进行多次操作,缓解大数据量的问题;
  • 咨询大数据团队:寻求大数据部门团队的专业支持,对于处理海量数据他们是专业的,看能不能提供一些可参考的建议。


二、硬件配置

核心思路:
加大服务器内存,合理分配服务器的堆内存,并设置好弹性伸缩规则,当触发告警时自动伸缩扩容,保证系统的可用性。

2.1云服务器配置

以下是阿里云 ECS 管理控制台的编辑页面,可以对 CPU 和内存进行配置。在 ECS 实例伸缩组创建完成后,即可以根据业务规模去创建一个自定义伸缩配置,在业务量大的时候会触发自动伸缩。

阿里云 ECS 管理

如果是部署在私有云服务器,需要对具体的 JVM 参数进行调优的话,可能还得请团队的资深大佬、或者运维团队的老师来帮忙处理。


三、文章小结

本篇文章主要是记录一次线上 bug 的处理思路,在之后的文章中我会分享一些关于真实项目中
处理高并发、缓存的使用、异步/解耦
等内容,敬请期待。

那么今天的分享到这里就结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!

对于高并发系统来说,有三个重要的机制来保障其高效运行,它们分别是:
缓存、限流和熔断
。而缓存是排在最前面也是高并发系统之所以高效运行的关键手段,那么问题来了:缓存只使用 Redis 就够了吗?

1.冗余设计理念

当然不是,不要把所有鸡蛋放到一个篮子里,成熟的系统在关键功能实现时一定会考虑冗余设计,注意这里的冗余设计不是贬义词。

冗余设计是在系统或设备完成任务起
关键作用的地方
,增加一套以上完成相同功能的功能通道(or 系统)、工作元件或部件,以保证当该部分出现故障时,系统或设备仍能正常工作,以减少系统或者设备的故障概率,提高系统可靠性。

例如,飞机的设计,飞机正常运行只需要两个发动机,但在每台飞机的设计中可能至少会设计四个发动机,这就有冗余设计的典型使用场景,这样设计的目的是为了保证极端情况下,如果有一个或两个发动机出现故障,不会因为某个发动机的故障而引起重大的安全事故。

2.多级缓存概述

缓存功能的设计也是一样,我们在高并发系统中通常会使用多级缓存来保证其高效运行,其中的多级缓存就包含以下这些:

  1. 浏览器缓存
    :它的实现主要依靠 HTTP 协议中的缓存机制,当浏览器第一次请求一个资源时,服务器会将该资源的相关缓存规则(如 Cache-Control、Expires 等)一同返回给客户端,浏览器会根据这些规则来判断是否需要缓存该资源以及该资源的有效期。
  2. Nginx 缓存
    :在 Nginx 中配置中开启缓存功能。
  3. 分布式缓存
    :所有系统调用的中间件都是分布式缓存,如 Redis、MemCached 等。
  4. 本地缓存
    :JVM 层面,单系统运行期间在内存中产生的缓存,例如 Caffeine、Google Guava 等。

以下是它们的具体使用。

2.1 开启浏览器缓存

在 Java Web应用中,实现浏览器缓存可以使用 HttpServletResponse 对象来设置与缓存相关的响应头,以开启浏览器的缓存功能,它的具体实现分为以下几步。

① 配置 Cache-Control

Cache-Control 是 HTTP/1.1 中用于控制缓存策略的主要方式。它可以设置多个指令,如 max-age(定义资源的最大存活时间,单位秒)、no-cache(要求重新验证)、public(指示可以被任何缓存区缓存)、private(只能被单个用户私有缓存存储)等,设置如下:

response.setHeader("Cache-Control", "max-age=3600, public"); // 缓存一小时

② 配置 Expires

设置一个绝对的过期时间,超过这个时间点后浏览器将不再使用缓存的内容而向服务器请求新的资源,设置如下:

response.setDateHeader("Expires", System.currentTimeMillis() + 3600 * 1000); // 缓存一小时

③ 配置 ETag

ETag(实体标签)一种验证机制,它为每个版本的资源生成一个唯一标识符。当客户端发起请求时,会携带上先前接收到的 ETag,服务器根据 ETag 判断资源是否已更新,若未更新则返回 304 Not Modified 状态码,通知浏览器继续使用本地缓存,设置如下:

String etag = generateETagForContent(); // 根据内容生成ETag
response.setHeader("ETag", etag);

④ 配置 Last-Modified

指定资源最后修改的时间戳,浏览器下次请求时会带上 If-Modified-Since 头,服务器对比时间戳决定是否返回新内容或发送 304 状态码,设置如下:

long lastModifiedDate = getLastModifiedDate();
response.setDateHeader("Last-Modified", lastModifiedDate);

整体配置

在 Spring Web 框架中,可以通过 HttpServletResponse 对象来设置这些头信息。例如,在过滤器中设置响应头以启用缓存:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
       throws IOException, ServletException {
   HttpServletResponse httpResponse = (HttpServletResponse) response;
   // 设置缓存策略
   httpResponse.setHeader("Cache-Control", "max-age=3600");

   // 其他响应头设置...
   chain.doFilter(request, response);
}

以上就是在 Java Web 应用程序中利用 HTTP 协议特性控制浏览器缓存的基本方法。

2.2 开启 Nginx 缓存

Nginx 中开启缓存的配置总共有以下 5 步。

① 定义缓存配置

在 Nginx 配置中定义一个缓存路径和配置,通过 proxy_cache_path 指令完成,例如,以下配置:

proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

其中:

  • /path/to/cache:这是缓存文件的存放路径。
  • levels=1:2:定义缓存目录的层级结构。
  • keys_zone=my_cache:10m:定义一个名为
    my_cache
    的共享内存区域,大小为 10MB。
  • max_size=10g:设置缓存的最大大小为 10GB。
  • inactive=60m:如果在 60 分钟内没有被访问,缓存将被清理。
  • use_temp_path=off:避免在文件系统中进行不必要的数据拷贝。

② 启用缓存

在 server 或 location 块中,使用 proxy_cache 指令来启用缓存,并指定要使用的 keys zone,例如,以下配置:

server {  
    ...  
    location / {  
        proxy_cache my_cache;  
        ...  
    }  
}

③ 设置缓存有效期

使用 proxy_cache_valid 指令来设置哪些响应码的缓存时间,例如,以下配置:

location / {  
    proxy_cache my_cache;  
    proxy_cache_valid 200 304 12h;  
    proxy_cache_valid any 1m;  
    ...  
}

④ 配置反向代理

确保你已经配置了反向代理,以便 Nginx 可以将请求转发到后端服务器。例如,以下配置:

location / {  
    proxy_pass http://backend_server;  
    ...  
}

⑤ 重新加载配置

保存并关闭 Nginx 配置文件后,使用 nginx -s reload 命令重新加载配置,使更改生效。

2.3 使用分布式缓存

在 Spring Boot 项目中使用注解的方式来操作分布式缓存 Redis 的实现步骤如下。

① 添加依赖

在你的 pom.xml 文件中添加 Spring Boot 的 Redis 依赖,如下所示:

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

② 配置 Redis 连接信息

在 application.properties 或 application.yml 文件中配置 Redis 的相关信息,如下所示。

# application.properties  
spring.redis.host=localhost  
spring.redis.port=6379

③ 启动缓存

在 Spring Boot 主类或者配置类上添加 @EnableCaching 注解来启用缓存。

import org.springframework.cache.annotation.EnableCaching;  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
  
@SpringBootApplication  
@EnableCaching  
public class Application {  
  
    public static void main(String[] args) {  
        SpringApplication.run(Application.class, args);  
    }  
  
}

④ 使用缓存

在服务类或方法上使用 @Cacheable,@CacheEvict,@CachePut 等注解来定义缓存行为。

例如,使用 @Cacheable 注解来缓存方法的返回值:

import org.springframework.cache.annotation.Cacheable;  
import org.springframework.stereotype.Service;  
  
@Service  
public class UserService {  
    @Cacheable("users")  
    public User findUserById(Long id) {  
        // 模拟从数据库中查询用户  
        return new User(id, "Alice");  
    }  
}

也可以使用 @CacheEvict 注解来删除缓存:

import org.springframework.cache.annotation.CacheEvict;  
import org.springframework.stereotype.Service;  
  
@Service  
public class UserService {  
    @CacheEvict(value = "users", key = "#id")  
    public void deleteUser(Long id) {  
        // 模拟从数据库中删除用户  
    }  
}

在这个例子中,deleteUser 方法会删除 "users" 缓存中 key 为 id 的缓存项。

可以使用 @CachePut 注解来更新缓存:

import org.springframework.cache.annotation.CachePut;  
import org.springframework.stereotype.Service;  
  
@Service  
public class UserService {  
  
    @CachePut(value = "users", key = "#user.id")  
    public User updateUser(User user) {  
        // 模拟更新数据库中的用户信息  
        return user;  
    }  
  
}

在这个例子中,updateUser 方法会更新 "users" 缓存中 key 为 user.id 的缓存项,缓存的值是方法的返回值。

2.4 使用本地缓存

以 Caffeine 本地缓存的使用为例,它在 Spring Boot 项目中的使用如下。

① 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

② 配置 Caffeine 缓存

在 application.properties 或 application.yml 文件中配置 Caffeine 缓存的相关参数。例如:

# application.properties
spring.cache.type=caffeine
spring.cache.caffeine.spec=initialCapacity=100,maximumSize=1000,expireAfterWrite=10s

这里 spring.cache.caffeine.spec 是一个 Caffeine 规范字符串,用于设置初始容量、最大容量和写入后过期时间等缓存策略,其中:

  • initialCapacity:初始容器容量。
  • maximumSize:最大容量。
  • expireAfterWrite:写入缓存后 N 长时间后过期。

③ 自定义 Caffeine 配置类(可选步骤)

如果需要更复杂的配置,可以创建一个 Caffeine CacheManager 的配置类:

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineCacheConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager() {
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(1000)
                .expireAfterWrite(10, TimeUnit.SECONDS) // 10 秒后过期
                .recordStats(); // 记录缓存统计信息

        return new CaffeineCacheManager("default", caffeine::build);
    }

    @Override
    public CacheResolver cacheResolver() {
        // 自定义缓存解析器(如果需要)
        // ...
        return super.cacheResolver();
    }
}

④ 开启缓存

若要利用 Spring Cache 抽象层,以便通过注解的方式更方便地管理缓存,需要在启动类上添加 @EnableCaching 注解,如下所示:

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableCaching
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

⑤ 使用注解进行缓存操作

在业务逻辑类中使用 @Cacheable、@CacheEvict 等注解实现数据的缓存读取和更新,和上面分布式缓存的使用相同,具体示例如下:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable(value = "users", key = "#id") // 假设我们有一个名为"users"的缓存区域
    public User getUserById(Long id) {
        // 这里是真实的数据库查询或其他耗时操作
        return userRepository.findById(id).orElse(null);
    }

    @CacheEvict(value = "users", key = "#user.id")
    public void updateUser(User user) {
        userRepository.save(user);
    }
}

课后思考

除了以上的缓存之外,还有哪些缓存可以加速程序的执行效率呢?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

Sora文生视频模型深度剖析:全网独家指南,洞悉98%关键信息,纯干货

Sora是一个以视频生成为核心的多能力模型,具备以下能力:

  • 文/图生成视频
  • 视频生成视频
  • 1分钟超长高质量视频生成
  • 视频裂变多视角生成
  • 准工业级数字孪生游戏/科幻片等特效,物理引擎能力

1.Sora 与 Runway Gen2、Pika 等能力差异对比

能力项 OpenAl Sora 其它模型
视频时长 60 秒 最多十几秒
视频长宽比 1920x1080 与 1080x1920 之间任意尺寸 固定尺寸, 如 16:9,9:16,1:1 等
视频清晰度 1080P upscale 之后达到 4K
文本生成视频 支持 支持
图片生成视频 支持 支持
视频生成视频 支持 支持
多个视频链接 支持 不支持
文本编辑视频 支持 支持
扩展视频 向前 / 向后扩展 仅支持向后扩展
视频连接 支持 不支持
真实世界模拟 支持 支持
运动相机模拟
依赖关系进行建模
影响世界状态 (世界交互)
人工过程 (数字世界) 模拟 支持 不支持
  1. 视频清晰度,OpenAI Sora 默认是 1080P,而且其它平台大多数默认的清晰度也都是 1080P 以下,只是在经过 upscale 等操作之后可以达到更清晰的水平。
  2. Sora 开箱即用生成60s 的时长视频,其中视频连接、数字世界模拟、影响世界状态(世界交互)、运动相机模拟等都是此前视频平台或者工具中不具备的。
  3. OpenAI Sora 模型还可以直接生成图片,它是一个以视频生成为核心的多能力模型。

2. Sora技术突破点

sora 是一个以 latent、transformer、diffusion 为三个关键点的模型。

  • 官网案例展示


    • 提示词:“两艘海盗船在一杯咖啡内航行时互相战斗的逼真特写视频。”


      • 模拟器实例化了两种精美的 3D 资产:具有不同装饰的海盗船。 Sora 必须在其潜在空间中隐式地解决文本到 3D 的问题。
      • 3D 对象在航行并避开彼此路径时始终保持动画效果。
      • 咖啡的流体动力学,甚至是船舶周围形成的泡沫。流体模拟是计算机图形学的一个完整子领域,传统上需要非常复杂的算法和方程。
      • 照片写实主义,几乎就像光线追踪渲染一样。
      • 模拟器考虑到杯子与海洋相比尺寸较小,并应用移轴摄影来营造 “微小” 的氛围。
      • 场景的语义在现实世界中并不存在,但引擎仍然实现了我们期望的正确物理规则。
    • 提示词:一位时尚的女人走在东京的街道上,街道上到处都是温暖的发光霓虹灯和动画城市标志。她身穿黑色皮夹克,红色长裙,黑色靴子,背着一个黑色钱包。她戴着墨镜,涂着红色口红。她自信而随意地走路。街道潮湿而反光,营造出五颜六色的灯光的镜面效果。许多行人四处走动

      视频链接:
      https://live.csdn.net/v/364231

    • 自主创建多个视角的视频



    世界模型和物理引擎是虚拟现实(VR)和计算机图形学中的两个关键概念。世界模型是描述虚拟环境的框架,包括场景、对象、光照等元素,用于呈现虚拟世界的外观和感觉。物理引擎则是用于模拟和计算物体之间的物理运动和互动,如重力、碰撞、摩擦等。简而言之,世界模型是虚拟环境的静态描述,而物理引擎则负责模拟虚拟环境中物体的动态行为。它们共同作用于虚拟现实技术中,为用户提供沉浸式的体验。


    世界模型要求更高,这包括对复杂场景和物理效果的处理能力、提高在新环境中的泛化能力、以及更好地利用先验知识进行实时推理、预测和决策等。虽然 Sora 已经能够生成较为准确的视频内容,但当场景中涉及到多个物体的交互或复杂的物理运动时,Sora 可能会出现失误或偏差。其次 Sora 目前主要依赖于大量的训练数据来学习视频的生成规律,但这种方式可能限制了其在新环境中的泛化能力和实时决策能力。这也是目前 Sora 并非一个世界模型的原因

从 Sora 模型的技术报告中,我们可以看到 Sora 模型的实现,是建立在 OpenAI 一系列坚实的历史技术工作的沉淀基础上的包括不限于视觉理解(Clip),Transformers 模型和大模型的涌现(ChatGPT),Video Caption(DALL·E 3)

2.1 核心点1:视频压缩网络

patches 是从大语言模型中获得的灵感,大语言模型范式的成功部分得益于使用优雅统一各种文本模态(代码、数学和各种自然语言)的 token。大语言模型拥有文本 token,而 Sora 拥有视觉分块(patches)。OpenAI 在之前的 Clip 等工作中,充分实践了分块是视觉数据模型的一种有效表示(参考论文:An image is worth 16x16 words: Transformers for image recognition at scale.)这一技术路线。而视频压缩网络的工作就是将高维度的视频数据转换为 patches,首先将视频压缩到一个低纬的 latent space,然后分解为 spacetime patches。

难点:视频压缩网络类比于 latent diffusion model 中的 VAE,但是压缩率是多少,如何保证视频特征被更好地保留,还需要进一步的研究。

2.2 核心点2:长视频的 scaling transformer

给定输入的噪声块 + 文本 prompt,它被训练来预测原始的 “干净” 分块。重要的是,Sora 是一个 Scaling Transformers。Transformers 在大语言模型上展示了显著的扩展性,

难点:能够 scaling up 的 transformer 如何训练出来,对第一步的 patches 进行有效训练,可能包括的难点有 long context(长达 1 分钟的视频)的支持、期间 error accumulation 如何保证比较低,视频中实体的高质量和一致性,video condition,image condition,text condition 的多模态支持等。

2.3 核心点3:Video recaption

视频摘要 / 视频字母生成属于多模态学习下的一个子任务,大体目标就是根据视频内容给出一句或多句文字描述。所生成的 caption 可用于后续的视频检索等等,也可以直接帮助智能体或者有视觉障碍的人理解现实情况。通过这样的高质量的训练数据,保障了文本(prompt)和视频数据之间高度的 align。Sora 还使用 DALL·E 3 的 recaption技巧,即为视觉训练数据生成高度描述性的 caption,这让 Sora 能够更忠实地遵循生成视频中用户的文本指令,而且会支持长文本,这个应该是 OpenAI 独有的优势。在生成阶段,Sora 会基于 OpenAI 的 GPT 模型对于用户的 prompt 进行改写,生成高质量且具备很好描述性的高质量 prompt,再送到视频生成模型完成生成工作。caption 训练数据都匮乏:

  1. 一方面,图像常规的文本描述往往过于简单(比如 COCO 数据集),它们大部分只描述图像中的主体而忽略图像中其它的很多信息,比如背景,物体的位置和数量,图像中的文字等。
  2. 另外一方面,目前训练文生图的图像文本对数据集(比如 LAION 数据集)都是从网页上爬取的,图像的文本描述其实就是 alt-text,但是这种文本描述很多是一些不太相关的东西,比如广告。

技术突破
:训练一个 image captioner 来合成图像的 caption,合成 caption 与原始 caption 的混合比例高达 95%:5%;但是不过采用 95% 的合成长 caption 来训练,得到的模型也会 “过拟合” 到长 caption 上,如果采用常规的短 caption 来生成图像,效果可能就会变差。为了解决这个问题,OpenAI 采用 GPT-4 来 “upsample” 用户的 caption,下面展示了如何用 GPT-4 来进行这个优化,不论用户输入什么样的 caption,经过 GPT-4 优化后就得到了长 caption:

难点:这项技术并不新,难的是积累,即便是合成数据也需要大量的专业标注和评测。“大” 模型,“高” 算力,“海量” 数据

更多内容见:探索AI视频生成新纪元:文生视频Sora VS RunwayML、Pika及StableVideo——谁将引领未来:
https://blog.csdn.net/sinat_39620217/article/details/136171409

3.sora存在不足

  1. 物理交互逻辑错误
    :Sora 有时会创造出物理上不合理的动作; Sora 模型在模拟基本物理交互,如玻璃破碎等方面,不够精确。这可能是因为模型在训练数据中缺乏足够的这类物理事件的示例,或者模型无法充分学习和理解这些复杂物理过程的底层原理
  2. 对象状态变化的不正确
    :在模拟如吃食物这类涉及对象状态显著变化的交互时,Sora 可能无法始终正确反映出变化。这表明模型可能在理解和预测对象状态变化的动态过程方面存在局限。
  3. 复杂场景精确性丢失
    :模拟多个对象和多个角色之间的复杂互动会出现超现实结果; 长时视频样本的不连贯性:在生成长时间的视频样本时,Sora 可能会产生不连贯的情节或细节,这可能是由于模型难以在长时间跨度内保持上下文的一致性 ; 对象的突然出现:视频中可能会出现对象的无缘无故出现,这表明模型在空间和时间连续性的理解上还有待提高

4.文生视频prompt优化

视频 官方提示词 优化
逼真的特写视频,展示两艘海盗在一杯咖啡内航行时互相争斗的情况。 Context
:一杯啡内的微型世界。
Persona
: 两艘海盗船。
Goal
: 展示海盗船在咖杆内的逼真争斗场景。
Constraints
:视频应突出海盗船的细节和动态,以及咖啡的纹理作为背景。
Steps
:设定场景为充满咖啡的杯子,咖啡表面作为海洋。描述海盗船:两艘细致的海盗船在咖啡 “海洋” 中航行和争斗。强调特写头:使用特写镜头视角捕捉海盗船的动态和咖啡的纹理。展现争斗细爷:海盗船回的交火,船上海盗的动作。
Examples
: 相似效果链接
Template
:cssCopy Code
[场景描述]
在一杆充满就的杯子中,咖啡表面波动着仿佛一个微型的海洋。
[人物描述]
两艘装备精良的海盗船在这杯咖啡的海洋中航行,互相展开烈的争斗。
[目标]
透过逼真的特写镜头展现海盗船在咖啡杯子内互相争斗的壮场景。
[约束条件]
注意捕浞海盗的细节和动态,以及咖啡作为背景的纹理和波动
一位时尚女性走在充满温暖霓虹灯和动画城市标牌的东京街道上。她穿着黑色皮夹克红色长裙和黑色子,拎黑色钱包。她戴着太阳墨镜涂着红色囗红。她走路自信又随意。街道潮湿且反光,在影色灯光的照射下形成镜面效果。许多行人走来走去。 Context
: 一条充满活力的东京街道在夜晚灯火通明,霓虹灯和动画广告牌交织成一道道流光溢彩的光带。细雨过后的街道湿润且反光,在多彩的灯光照射下形成迷人的镜面效果。许多行人在这灯光闪烁的夜色中来往匆匆。
Persona
: 一位时尚女性身着黑色皮夹克,搭配鲜艳的红色长裙和黑色靴子,手拎一只黑色钱包。她戴着太阳镜,嘴唇涂抹着红色口红,走路自信又洒脱。
Goal
: 展示这位时尚女性在霓虹灯光点缀的东京夜晚中自信与风采。
Constraints
: 视觉应该突出夜晚的霓虹灯光效果,反映出潮湿街道的反光效果,以及人物的时尚装扮,强调人物的自信步伐和随性的走路风格。-
Steps
::
1
.设定场景为东京的一个夜晚街道,由霓虹灯照明。
2
.描述人物:一位穿着黑色皮夹克、红色长裙和黑色靴子的时尚女性,手拿黑色钱包,戴着太阳镜并涂有红色口红。
3
.强调人物的自信步伐和随性的走路风格。
4
.描述环境:潮湿的街道在灯光下反射,周围有行人。示例: 提供一段描述或者图片,展示类似场景的效果。
Template
:cssCopy Code:
[场景描述]
在一个充满活力的街道上,霓虹灯的彩光波动着,仿佛一个微型的夜晚海洋。
[人物描述]
一位时尚女性在这条街道上自信地行走,她的黑夹克和红裙在灯光下显得格外抢眼。
[目标]
通过鲜明的场景描述,展现时尚女性在霓虹灯光下的自信与风采。
[约束条件]
注重捕捉人物装扮的细节和动态,以及潮湿街道作为背景的纹理和反光。

5.Sora的出现以及AI的出现会对程序员产生什么影响呢

  • 积极影响:


    • 提高编程效率:AI工具可以自动化一些繁琐的编程任务,如代码检查、代码重构等,从而减少了程序员的工作量,提高了编程效率。同时,Sora文生视频也为程序员提供了更加高效和智能的视频开发工具,可以加快开发速度。
    • 改善代码质量:AI工具可以帮助程序员发现代码中的缺陷和潜在问题,提高代码的质量和可靠性。这对于保证软件质量和用户体验至关重要。
    • 促进编程教育:AI工具和Sora文生视频可以为编程初学者提供更加友好的编程环境和工具,使得编程教育更加容易上手和有趣,从而吸引更多的人加入编程领域。
    • 提供更多创新机会:AI工具可以为程序员提供更多的灵感和创意,帮助他们创造出更加优秀的程序。同时,Sora文生视频也为程序员提供了更多的应用场景和市场需求,从而激发他们的创新热情。
  • 负面影响:


    • 职业竞争压力增加:随着AI技术的发展,一些简单的编程任务可能会被自动化工具所取代,这就要求程序员需要不断学习和掌握新的技能,以适应技术变革的需要。这可能会导致职业竞争压力增加,一些技能不足的程序员可能会面临失业的风险。
    • 道德和伦理挑战:AI工具的发展和应用也带来了一些道德和伦理问题,如数据隐私、算法公平性等。程序员需要关注这些问题,并在开发过程中遵守相关的法律法规和道德规范。

6.Sora 技术原理全解析&小结

OpenAI 的研究论文《Video generation models as world simulators》探讨了在视频数据上进行大规模训练生成模型的方法。这项研究特别关注于文本条件扩散模型,这些模型同时在视频和图像上进行训练,处理不同时长、分辨率和宽高比的数据。研究中提到的最大模型 Sora 能够生成长达一分钟的高保真视频。以下是论文的一些关键点:

  1. 统一的视觉数据表示
    :研究者们将所有类型的视觉数据转换为统一的表示,以便进行大规模的生成模型训练。Sora 使用视觉补丁(patches)作为其表示方式,类似于大型语言模型(LLM)中的文本标记。

  2. 视频压缩网络
    :研究者们训练了一个网络,将原始视频压缩到一个低维潜在空间,并将其表示分解为时空补丁。Sora 在这个压缩的潜在空间中进行训练,并生成视频。

  3. 扩散模型
    :Sora 是一个扩散模型,它通过预测原始“干净”的补丁来从输入的噪声补丁中生成视频。扩散模型在语言建模、计算机视觉和图像生成等领域已经显示出了显著的扩展性。

  4. 视频生成的可扩展性
    :Sora 能够生成不同分辨率、时长和宽高比的视频,包括全高清视频。这种灵活性使得 Sora 能够直接为不同设备生成内容,或者在生成全分辨率视频之前快速原型化内容。

  5. 语言理解
    :为了训练文本到视频生成系统,需要大量的视频和相应的文本标题。研究者们应用了在 DALL·E 3 中引入的重新描述技术,首先训练一个高度描述性的标题生成器,然后为训练集中的所有视频生成文本标题。

  6. 图像和视频编辑
    :Sora 不仅能够基于文本提示生成视频,还可以基于现有图像或视频进行提示。这使得 Sora 能够执行广泛的图像和视频编辑任务,如创建完美循环的视频、动画静态图像、向前或向后扩展视频等。

  7. 模拟能力
    :当视频模型在大规模训练时,它们展现出了一些有趣的新兴能力,使得 Sora 能够模拟物理世界中的某些方面,如动态相机运动、长期一致性和对象持久性等。

尽管 Sora 展示了作为模拟器的潜力,但它仍然存在许多局限性,例如在模拟基本物理交互(如玻璃破碎)时的准确性不足。研究者们认为,继续扩展视频模型是开发物理和数字世界模拟器的有前途的道路。
这篇论文提供了对 Sora 模型的深入分析,展示了其在视频生成领域的潜力和挑战。通过这种方式,OpenAI 正在探索如何利用 AI 来更好地理解和模拟我们周围的世界。

更多优质内容请关注公号:汀丶人工智能;会提供一些相关的资源和优质文章,免费获取阅读。

  • 参考链接:

探索AI视频生成新纪元:文生视频Sora VS RunwayML、Pika及StableVideo——谁将引领未来:
https://blog.csdn.net/sinat_39620217/article/details/136171409

stable-diffusion-videos:
https://github.com/nateraw/stable-diffusion-videos

StableVideo:
https://github.com/rese1f/StableVideo

sora官网:
https://openai.com/sora

sora报告的链接:
https://openai.com/research/video-generation-models-as-world-simulators

我想很多人已经体验过GRPC提供的三种流式消息交换(Client Stream、Server Stream和Duplex Stream)模式,在.NET Core上构建的GRPC应用本质上是采用HTTP2/HTTP3协议的ASP.NET Core应用,我们当然也可以在一个普通的ASP.NET Core应用实现这些流模式。不仅如此,HttpClient也提供了响应的支持,这篇文章通过一个简单的实例提供了相应的实现, 源代码从 这里 下载。

一、双向流的效果
二、[服务端]流式请求/响应的读写
三、[客户端]流式响应/请求的读写

一、双向流的效果

在提供具体实现之前,我们不妨先来演示一下最终的效果。我们通过下面这段代码构建了一个简单的ASP.NET Core应用,如代码片段所示,在调用WebApplication的静态方法CreateBuilder将WebApplicationBuilder创建出来后,我们调用其扩展方法UseKestrel将默认终结点的监听协议设置为Http1AndHttp2AndHttp3,这样我们的应用将提供针对不同HTTP协议的全面支持。

var url = "http://localhost:9999";
var builder = WebApplication.CreateBuilder(args);
builder.WebHost
    .UseKestrel(kestrel=> kestrel.ConfigureEndpointDefaults(listen=>listen.Protocols = HttpProtocols.Http1AndHttp2AndHttp3))
    .UseUrls(url);
var app = builder.Build();
app.MapPost("/", httpContext=> HandleRequestAsync(httpContext, async (request, writer) => {
    Console.WriteLine($"[Server]Receive request message: {request}");
    await writer.WriteStringAsync(request);
}));
await app.StartAsync();

await SendStreamRequestAsync(url, ["foo", "bar", "baz", "qux"], reply => {
    Console.WriteLine($"[Client]Receive reply message: {reply}\n");
    return Task.CompletedTask;
});

我们针对根路径(/)注册了一个HTTP方法为POST的路由终结点,终结点处理器调用HanleRequestAsync来处理请求。这个方法提供一个Func<string, PipeWriter, Task>类型的参数作为处理器,该委托的第一个参数表示接收到的单条请求消息,PipeWriter用来写入响应内容。在这里我们将接收到的消息进行简单格式化后将其输出到控制台上,随之将其作为响应内容进行回写。

在应用启动之后,我们调用
SendStreamRequestAsync方法以流的方式发送请求,并处理接收到的响应内容。该方法的第一个参数为请求发送的目标URL,第二个参数是一个字符串数组,我们将以流的方式逐个发送每个字符串。最后的参数是一个Func<string,Task>类型的委托,用来处理接收到的响应内容(字符串),在这里我们依然是将格式化的响应内容直接打印在控制台上。

image

程序启动后控制台上将出现如上图所示的输出,客户端/服务端接收内容的交错输出体现了我们希望的“双向流式”消息交换模式。我们将在后续介绍HanleRequestAsync和
SendStreamRequestAsync
方法的实现逻辑。

二、[服务端]流式请求/响应的读写

HanleRequestAsync方法定义如下。如代码片段所示,我们利用请求的BodyReader和响应的BodyWriter来对请求和响应内容进行读写,它们的类型分别是PipeReader和PipeWriter。在一个循环中,在利用BodyReader将请求缓冲区内容读取出来后,我们将得到的ReadOnlySequence<byte>对象作为参数调用辅助方法TryReadMessage读取单条请求消息,并调用handler参数表示的处理器进行处理。当请求内容接收完毕后,循环终止。

static async Task HandleRequestAsync(HttpContext httpContext, Func<string, PipeWriter, Task> handler)
{
    var reader = httpContext.Request.BodyReader;
    var writer = httpContext.Response.BodyWriter;
    while (true)
    {
        var result = await reader.ReadAsync();
        var buffer = result.Buffer;
        while (TryReadMessage(ref buffer, out var message))
        {
            await handler(message, writer);
        }
        reader.AdvanceTo(buffer.Start, buffer.End);
        if (result.IsCompleted)
        {
            break;
        }
    }
}

由于客户端发送的单条字符串消息长度不限,为了精准地将其读出来,我们需要在输出编码后的消息内容前添加4个字节的整数来表示消息的长度。所以在如下所示的TryReadMessage方法中,我们会先将字节长度读取出来,再据此将消息自身内容读取出来,最终通过解码得到消息字符串。

static bool TryReadMessage(ref ReadOnlySequence<byte> buffer, [NotNullWhen(true)]out string? message)
{
    var reader = new SequenceReader<byte>(buffer);
    if (!reader.TryReadLittleEndian(out int length))
    {
        message = default;
        return false;
    }

    message = Encoding.UTF8.GetString(buffer.Slice(4, length));
    buffer = buffer.Slice(length + 4);
    return true;
}

响应消息的写入是通过如下针对PipeWriter的WriteStringAsync扩展方法实现的,这里的PipeWriter就是响应的BodyWriter,针对“Length + Payload“的消息写入也体现在这里。

public static class Extensions
{
    public static ValueTask<FlushResult> WriteStringAsync(this PipeWriter writer, string content)
    {
        var length = Encoding.UTF8.GetByteCount(content);
        var span = writer.GetSpan(4 + length);
        BitConverter.TryWriteBytes(span, length);
        Encoding.UTF8.GetBytes(content, span.Slice(4));
        writer.Advance(4 + length);
        return writer.FlushAsync();
    }
}

三、[客户端]流式响应/请求的读写

客户端利用HttpClient发送请求。针对HttpClient的请求通过一个HttpRequestMessage对象表示,其主体内容体现为一个HttpContent。流式请求的发送是通过如下这个StreamContent类型实现的,它派生于HttpContent。我们重写了SerializeToStreamAsync方法,利用自定义的StreamContentWriter将内容写入请求输出流。

public class StreamContent(StreamContentWriter writer) : HttpContent
{
    private readonly StreamContentWriter _writer = writer;
    protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)
=> _writer.SetOutputStream(stream).WaitAsync(); protected override bool TryComputeLength(out long length) => (length = -1) != -1; } public class StreamContentWriter { private readonly TaskCompletionSource<Stream> _streamSetSource = new(); private readonly TaskCompletionSource _streamEndSource = new(); public StreamContentWriter SetOutputStream(Stream outputStream) { _streamSetSource.SetResult(outputStream); return this; } public async Task WriteAsync(string content) { var stream = await _streamSetSource.Task; await PipeWriter.Create(stream).WriteStringAsync(content); } public void Complete() => _streamEndSource.SetResult(); public Task WaitAsync() => _streamEndSource.Task; }

StreamContentWriter提供了四个方法,SetOutputStream方法用来设置请求输出流,上面重写的SerializeToStreamAsync调用了此方法。单条字符串消息的写入实现在WriteAsync方法中,它最终调用的依然是上面提供的WriteStringAsync扩展方法。整个流式请求的过程通过一个TaskCompletionSource对象提供的Task来表示,当客户端完成所有输出后,会调用Complete方法,该方法进一步调用这个TaskCompletionSource对象的SetResult方法。由于WaitAsync方法返回TaskCompletionSource对象提供的Task,SerializeToStreamAsync方法会调用此方法等待”客户端输出流“的终结。

如下的代码片段体现了SendStreamRequestAsync方法的实现。在这里我们创建了一个表示流式请求的HttpRequestMessage对象,我们将协议版本设置为HTTP2,作为主体内容的HttpContent正式根据StreamContentWriter对象创建的StreamContent对象。

static async Task SendStreamRequestAsync(string url,string[] lines, Func<string, Task> handler)
{
    using var httpClient = new HttpClient();
    var writer = new StreamContentWriter();
    var request = new HttpRequestMessage(HttpMethod.Post, url)
    {
        Version = HttpVersion.Version20,
        VersionPolicy = HttpVersionPolicy.RequestVersionExact,
        Content = new StreamingWeb.StreamContent(writer)
    };
    var task = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
    _ = Task.Run(async () =>
    {
        var response = await task;
        var reader = PipeReader.Create(await response.Content.ReadAsStreamAsync());
        while (true)
        {
            var result = await reader.ReadAsync();
            var buffer = result.Buffer;
            while (TryReadMessage(ref buffer, out var message))
            {
                await handler(message);
            }
            reader.AdvanceTo(buffer.Start, buffer.End);
            if (result.IsCompleted)
            {
                break;
            }
        }
    });

    foreach (string line in lines)
    {
        await writer.WriteAsync($"{line} ({DateTimeOffset.UtcNow})");
        await Task.Delay(1000);
    }
    writer.Complete();
}

我们将这个HttpRequestMessage作为请求利用HttpClient发送出去,实际上发送的内容最终是通过调用StreamContentWriter对象的WriteAsync方法输出的,我们每隔1秒发送一条消息。HttpClient将请求发出去之后会得到一个通过HttpResponseMessage对象表示的响应,在一个异步执行的Task中,我们根据响应流创建一个PipeReader对象,并在一个循环中调用上面定义的TryReadMessage方法逐条读取接收到的单条消息进行处理。