面试官:“AI 9 秒删了生产库,你先干什么?” 我:“先关 Agent 权限。” 面试官:“备份也被它删了,怎么办?”

0 阅读9分钟

9 秒,生产库没了

最近鸭鸭刷到一个挺吓人的 AI 事故。

不是 AI 写错了一个函数,也不是代码 review 漏了一个边界。

而是据多家媒体报道,一家海外创业公司的生产数据库,被 AI Agent 在 9 秒内删掉了。

更离谱的是,连备份也一起没了。

file_1777465037091_990

公开报道里大概是这么个过程:

公司创始人在测试环境里让 Cursor Agent 处理一个常规运维任务。Agent 遇到凭据不匹配,没有停下来问人,而是自己去代码库里翻 token。

它翻到了一个 Railway API token。

然后直接发了删除卷的 GraphQL mutation。

9 秒后,生产数据库没了。

创始人再去找备份,发现卷级备份和源数据在同一个卷里。卷删了,备份也跟着没了。最近能用的恢复点,是 3 个月前。

说实话,鸭鸭看到这里第一反应不是“AI 好可怕”。

是“这套权限设计也太敢了”。

……

评论区分成两派。

一派会说:看吧,AI 不能信,生产环境绝对不能让 Agent 碰。

另一派会说:这锅不该全甩 AI,token 权限太大、备份没隔离、删除没二次确认,这些才是根因。

鸭鸭更偏第二派。

这件事最值得程序员警惕的,不是 AI 会不会发疯。

而是我们正在把一个会猜、会搜索、会执行命令的东西,接进以前只有高级工程师才敢碰的生产权限里。

以前新人实习生要删库,至少得有人把账号给他、把命令教他、把生产环境入口打开。

现在不一样了。

Agent 能自己翻代码、自己找 token、自己拼 API、自己执行。

它不需要恶意。它只要“以为自己理解了”,就够吓人了。

所以鸭鸭觉得,这件事真正的独占视角是:

AI Agent 不是新同事,它更像一个执行速度极快、但没有责任感的运维账号。

你不能只问它聪不聪明。

你得问它能碰什么、不能碰什么、碰危险东西时谁来按确认键。

但这次删库事故补上了另一半:

AI 写代码只是第一阶段。

AI 开始执行生产操作,才是真正考验工程体系的时候。

以前我们担心的是:

“AI 写的代码有没有 bug?”

以后我们更该担心的是:

“AI 拿着生产权限的时候,会不会把 bug 直接变成事故?”

这中间差的不是模型能力,是工程护栏。

鸭鸭觉得,后面公司用 AI Agent,至少要有几个硬规矩:

  • 生产权限必须最小化:管理域名的 token,就不应该有删数据库卷的能力。
  • 破坏性操作必须人工确认:删库、删卷、清缓存、重建索引,这种动作不能靠提示词自觉。
  • 备份必须物理隔离:备份和源数据放在同一个可删除对象里,本质上就不叫备份。
  • Agent 操作必须可审计:谁授权的、它执行了什么、为什么执行,事后要能追。

这里面每一条,都不是 AI 时代才出现的新知识。

权限隔离、二次确认、灾备演练、操作审计,这些都是老掉牙的工程常识。

只是 AI Agent 把这些常识的重要性放大了。

因为人类工程师可能会犹豫,会问一句“这个真的是生产吗?”

Agent 不一定会。

它可能 9 秒就把你三个月的数据带走。

所以如果面试官问:

“AI 都能帮你运维了,后端工程师还剩什么价值?”

鸭鸭会这么答:

“写命令的价值变低了,决定哪些命令永远不能被随便执行的价值变高了。”

这句话听着有点绕,但特别现实。

未来真正值钱的程序员,不是会不会让 AI 多写几行代码,而是能不能给 AI 设计边界。

让它快,但不能让它乱来。

让它能干活,但不能让它拿 root 权限赌运气。

大家怎么看?你敢让公司的 AI Agent 碰生产环境吗?

……

今天鸭鸭和大家分享一道后端场景题面试题。

【在 Java 中,不使用锁如何实现一个线程安全的单例? 】

回答重点

不用锁指的是不能用 synchronizedLock 这些显式加锁的手段,但可以利用 JVM 自身的机制或者 CAS 原子操作来保证线程安全。

常见的实现方式有四种:

  1. 静态内部类:利用类加载机制,延迟加载且线程安全
  2. 枚举单例:JVM 保证线程安全,天然防反射破坏
  3. 饿汉式:类加载时直接初始化,简单粗暴
  4. CAS 无锁:用原子操作 AtomicReference 实现

img

推荐优先用静态内部类,既有延迟加载又不需要显式加锁,性能也好。枚举单例适合需要防御反射攻击的场景,比如配置中心的单例类。

扩展知识

静态内部类

这是工业界最常用的方式,核心原理是利用 JVM 的类加载机制。JVM 保证类的初始化过程是线程安全的,只会执行一次。

内部类 Holder 只有在第一次调用 getInstance() 时才会被加载,所以实现了延迟加载,不会像饿汉式那样类一加载就创建实例。

执行流程

  1. 第一次调用 getInstance() 时,触发 Holder 类加载
  2. JVM 执行 Holder 的 方法,初始化 INSTANCE
  3. JVM 保证 只执行一次且线程安全
  4. 后续调用直接返回已初始化的实例

代码实现

public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE; // 首次调用时加载 Holder 类并初始化实例
    }
}

优点是既有延迟加载,又不需要显式加锁,性能开销几乎为零。唯一的缺点是无法防止反射破坏,如果有人通过反射调用私有构造器,还是能创建多个实例。

枚举单例

JVM 在加载枚举类时,会保证每个枚举常量只实例化一次,实例化过程由虚拟机内部控制,天然线程安全。更重要的是,枚举单例可以防止反射和序列化破坏,因为 JVM 层面就禁止了对枚举构造器的反射调用。

代码实现:

public enum Singleton {
    INSTANCE; // 唯一实例
    
    public void doSomething() { 
        // 业务逻辑
    }
}

这是《Effective Java》里推荐的最佳实践。枚举单例不仅线程安全,还能防止反序列化时创建新对象,因为 JVM 在反序列化枚举时会直接返回已有的枚举常量,不会调用构造器。

缺点是不支持延迟加载,类一加载就会创建实例。如果单例对象很大或者初始化很耗时,可能会拖慢应用启动速度。

饿汉式

最简单的方式,类加载时直接初始化实例,依赖 JVM 类加载机制保证线程安全。

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

优点是实现简单,没有任何并发问题。缺点是不支持延迟加载,类一加载就创建实例,如果这个单例一直没被用到,就白白浪费了内存。适合小项目或者单例对象很轻量的场景。

CAS 无锁

通过原子操作 AtomicReference 实现无锁竞争,多个线程同时调用 getInstance() 时,只有一个线程能成功 CAS 设置实例,其他线程会继续循环重试。

代码实现:

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        while (true) {
            Singleton instance = INSTANCE.get();
            if (instance != null) {
                return instance;
            }
            instance = new Singleton();
            if (INSTANCE.compareAndSet(null, instance)) {
                return instance;
            }
        }
    }
}

这种方式的问题是 while(true) 会导致 CPU 空转,在高并发场景下可能有多个线程同时创建实例,虽然最终只有一个实例会被设置成功,但其他线程创建的实例会被丢弃,浪费了内存和 CPU。

实际项目中几乎不用这种方式,因为静态内部类已经足够好了。CAS 单例只在一些对性能要求极高、不能接受任何锁开销、且资源充足的场景下才会考虑。

为什么静态内部类是首选

静态内部类结合了饿汉式的线程安全和懒汉式的延迟加载,是目前工业界最常用的方案。原因是:

1)利用 JVM 类加载机制保证线程安全,不需要显式加锁,性能开销几乎为零。

2)Holder 类只有在第一次调用 getInstance() 时才会被加载,实现了延迟加载,避免了饿汉式的资源浪费。

3)实现简单,代码清晰,不容易出错。

4)没有 CAS 方式的 CPU 空转和内存浪费问题。

唯一需要注意的是,如果单例类需要防止反射破坏,就得用枚举单例。比如配置中心的单例类,如果被反射创建了多个实例,可能导致配置不一致。但大多数场景下,反射破坏是一个理论上的问题,实际开发中很少有人会故意用反射去破坏单例。

如何防止反射破坏

除了枚举单例天然防反射,其他方式都可以通过在构造器里加标志位来防御:

public class Singleton {
    private static volatile boolean initialized = false;
    
    private Singleton() {
        synchronized (Singleton.class) {
            if (initialized) {
                throw new RuntimeException("单例已被创建,禁止反射调用");
            }
            initialized = true;
        }
    }
    
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

这样如果有人通过反射调用构造器,第二次调用时会抛出异常,阻止创建多个实例。不过这种防御手段也不是绝对安全的,如果攻击者先通过反射修改 initialized 标志位,还是能绕过检查。所以需要真正安全的单例,还是得用枚举。

篇幅有限,更多 后端场景 相关面试题可以进入面试鸭(mianshiya.com)进行查阅