Java中用于生成唯一ID的序列生成器

13

我计划编写一个序列生成器,用于在我的REST资源实现类中进行POST操作时生成唯一的ID。由于每个POST请求都由单独的线程处理,因此我将变量设置为volatile并使方法同步。我没有使用传统关系型数据库提供的序列或其他选项。

public class SequenceGen {
    volatile static int n = 0;  
    public synchronized int nextNum(){
        return n++;
    }   
}

这是我迄今为止所做的,我计划在我的REST实现中创建一个名为SequenceGen的变量。我的实际问题是,它会在某个地方出错吗?我使用了两个线程进行测试,我没有看到任何重复的值。


1
nextNum 方法设置为 static 以确保它的正确性。 - Braj
7
为什么不直接使用 AtomicInteger - fge
这个字段应该是私有的。volatile 是多余的,因为你使用同步方法来访问它。但我同意 fge 的观点:AtomicInteger 是更好、更安全、更快速的解决方案。如果你计划有多个虚拟机,应该考虑使用 UUID(但你会得到一个字符串而不是整数)。 - JB Nizet
@JB Nizet,对于所有声明为volatile的变量(包括long和double变量),读取和写入操作都是原子性的。来源于http://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html - Ayan
@Ayan:读取是原子性的。写入也是原子性的。但 ++ 操作符会做一个读取,然后增加它的值,接着再写入。这个操作序列不是原子性的。因此两个线程可能会并行地读取相同的值,增加它,并并行地写入下一个相同的值。这就是为什么有 AtomicInteger.incrementAndGet() 存在的原因。 - JB Nizet
显示剩余7条评论
3个回答

27

它会起作用,但是AtomicInteger是一个内置类型,非常适合您的使用情况。

AtomicInteger seq = new AtomicInteger();
int nextVal = seq.incrementAndGet();

1
+1 还有一个关于使用AtomicInteger和序列的好帖子:https://dev59.com/nGYq5IYBdhLWcg3woB4W - paulk23

1
你可以利用 java.util.prefs.Preferences 将序列生成器的当前状态持久化到磁盘上,并在以后再次使用它。(此外,您可能希望使用多个序列生成器)。例如:
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.prefs.Preferences;

public final class SequenceGenerator {

    private static final Preferences PREFS = Preferences.userNodeForPackage(SequenceGenerator.class);
    private static final AtomicLong SEQ_ID = new AtomicLong(Integer.parseInt(PREFS.get("seq_id", "1")));
    private static final Map<Long, SoftReference<SequenceGenerator>> GENERATORS = new ConcurrentHashMap<>();
    private static final SequenceGenerator DEF_GENERATOR = new SequenceGenerator(0L, Long.parseLong(PREFS.get("seq_0", "1")));

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            GENERATORS.values().stream()
                    .map(SoftReference::get)
                    .filter(seq -> seq != null && seq.isPersistOnExit())
                    .forEach(SequenceGenerator::persist);
            if (DEF_GENERATOR.isPersistOnExit()) {
                DEF_GENERATOR.persist();
            }
            PREFS.put("seq_id", SEQ_ID.toString());
        }));
    }

    private final long sequenceId;
    private final AtomicLong counter;
    private final AtomicBoolean persistOnExit = new AtomicBoolean();

    private SequenceGenerator(long sequenceId, long initialValue) {
        this.sequenceId = sequenceId;
        counter = new AtomicLong(initialValue);
    }

    public long nextId() {
        return counter.getAndIncrement();
    }

    public long currentId() {
        return counter.get();
    }

    public long getSequenceId() {
        return sequenceId;
    }

    public boolean isPersistOnExit() {
        return persistOnExit.get();
    }

    public void setPersistOnExit(boolean persistOnExit) {
        this.persistOnExit.set(persistOnExit);
    }

    public void persist() {
        PREFS.put("seq_" + sequenceId, counter.toString());
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        GENERATORS.remove(sequenceId);
        if (persistOnExit.get()) {
            persist();
        }
    }

    @Override
    public int hashCode() {
        return Long.hashCode(sequenceId);
    }

    @Override
    public boolean equals(Object obj) {
        return obj == this || obj != null && obj instanceof SequenceGenerator && sequenceId == ((SequenceGenerator) obj).sequenceId;
    }

    @Override
    public String toString() {
        return "{" +
                "counter=" + counter +
                ", seq=" + sequenceId +
                '}';
    }

    public static SequenceGenerator getDefault() {
        return DEF_GENERATOR;
    }

    public static SequenceGenerator get(long sequenceId) {
        if (sequenceId < 0) {
            throw new IllegalArgumentException("(sequenceId = " + sequenceId + ") < 0");
        }
        if (sequenceId == 0) {
            return DEF_GENERATOR;
        }
        SoftReference<SequenceGenerator> r = GENERATORS.computeIfAbsent(sequenceId, sid -> {
            try {
                return new SoftReference<>(new SequenceGenerator(sid, Long.parseLong(PREFS.get("seq_" + sid, null))));
            } catch (Throwable t) {
                return null;
            }
        });
        return r == null ? null : r.get();
    }

    public static SequenceGenerator create() {
        return create(1);
    }

    public static SequenceGenerator create(long initialValue) {
        long sequenceId = SEQ_ID.getAndIncrement();
        SequenceGenerator seq = new SequenceGenerator(sequenceId, Long.parseLong(PREFS.get("seq_" + sequenceId, "" + initialValue)));
        GENERATORS.put(sequenceId, new SoftReference<>(seq));
        return seq;
    }

}

从这个实现中派生,这个版本 http://stackoverflow.com/questions/1186387/generating-9-digit-ids-without-database-sequence/42344842#42344842 支持缓存和服务器崩溃恢复。 - oraclesoon

1
如果你愿意使用 String 作为ID,而非int,那么你可以考虑使用 UUID (通用唯一标识符)。他们非常易于使用,正如它的名字所示,它们是唯一的。以下是生成UUID的示例:
// the value of uuid will be something like '03c9a439-fba6-41e1-a18a-4c542c12e6a8'
String uuid = java.util.UUID.randomUUID().toString()

UUID提供比int更好的安全性,因为使用整数时,您可以通过将1添加到请求ID来猜测下一个请求ID,但是UUID不是连续的,猜测任何其他人的请求ID的机会非常小。


1
尽管您的评论是正确的,但使用UUID会带来巨大的性能损失。 - Ioannis Deligiannis
@IoannisDeligiannis - 请详细说明一下。它会有多大的性能损失? - SergeyB
在我的情况下,性能非常差,所以我附加了一个分析器,发现它实际上是在访问硬盘。你可以在一个循环中添加它,并查看它对你的硬件有多慢。在下面的答案中,它是1100毫秒。 https://dev59.com/-2Uq5IYBdhLWcg3wY_ga - Ioannis Deligiannis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接