英文:
Need a locking a mechanism (Persistent lock) which will not be lost even if application restarts?
问题
我有以下情况。有两个应用程序共享一个数据库。这两个应用程序都可以用来修改底层数据库。例如,客户1可以从这两个系统中都进行修改。我想确保当有人在应用程序1中执行某项操作时,例如对客户1进行操作,我需要一个持久锁,以便来自应用程序2的任何人都不能对同一客户执行任何操作。即使其中任何一个应用程序崩溃,它仍然应保持锁定。对于解决这种问题,什么是正确的方法?
英文:
I have the following scenario. There are 2 applications that share a database. Both these applications can be used to alter the underlying database. For e.g., Customer 1 can be modified from both the systems. I want to make sure when someone performs an action on say customer 1 in application 1 then I need a persistent lock for that lock so that nobody from application 2 can perform any action on the same customer. Even if any of these applications go down, it should still hold the lock. What will be the right approach for solving such an issue?
答案1
得分: 2
正如 @Turing85 的评论所提示的,这是极其危险的领域:如果有人绊倒了电源线,您的应用程序将无法再次启动,除非有人手动解决问题。这很少是您想要的情况。
正常的解决方案是在数据库层面进行锁定:如果它是一个“单个文件是数据库”的模型,例如 H2 或 SQLite,那么让数据库引擎锁定文件进行写入,并将操作系统级别的文件锁视为您的控制机制。这有一个很大的优势,即如果应用程序 A 由于任何原因(电源短缺、硬件崩溃等)意外退出,锁定会被释放。
如果数据库是一个独立运行的进程(例如 psql、mysql、mssql 等),它们具有可供您使用的锁定功能。
如果没有这些选项可用,您可以自己编写锁定机制:您可以使用新的文件 API 创建保证原子/唯一性的文件:
int pid = 0; // 请参阅下面的说明
Path p = Paths.get("/绝对路径/到/约定的/锁定文件位置/lockfile.pid");
Files.write(p, String.valueOf(pid), StandardOpenOption.CREATE_NEW);
CREATE_NEW
打开选项是要求 Java 确保原子性:要么[A]文件之前不存在,现在存在,且是这个进程创建的,要么[B]会抛出异常。
没有[C]这个进程创建了它,但是另一个不幸正在同时执行相同的操作,并且其中一个进程现在正在覆盖另一个进程的工作——这就是 CREATE_NEW
的含义和保证:这不会发生(与 CREATE
选项不同,后者将覆盖已存在的内容并不提供原子性保证)。
现在,您可以将文件系统用作全局唯一锁:要获取锁定,只需创建该文件。如果可以,那太好了,您获得了锁定。如果不能,那么您必须等待(如果您关心尽快获取它,您需要使用监视器 API 或循环,但这不是一个很好的选择,与进程内锁定相比,这是一个非常昂贵的操作!)-要释放锁定,只需删除文件。
为防止硬崩溃导致文件被固定在那里,永久地阻止您的应用程序再次运行,可以注册“pid”(进程 ID)在其中。这在手动修复问题时会提供一些调试信息,并且您可以使用它来自动检查(“嘿,操作系统,是否仍然有一个 ID 为 123981 的进程在运行?没有?好的,那么它必须已经硬崩溃并留下了锁定文件!”)。不幸的是,使用 Java 处理进程 ID 相对复杂,因为 Java 更多地围绕着不应过多依赖底层操作系统的概念设计,而且 Java 实际上并不假定“进程 ID”是底层操作系统执行的操作。请搜索一下如何获取它,您是可以做到的。
这将我们带到最后一个问题,您明显担心的一致性问题:毕竟,您似乎想要一个非常疯狂的想法,即在发生硬崩溃时(进程崩溃且未明确释放锁定)永久禁用应用程序。我假设您之所以这样做,是因为您担心数据库处于不一致的状态,不希望任何东西再次触及它,直到您手动检查它。
好的,那么锁定文件就是您实现这一点的方法。然而,这对用户不友好,也没有必要:您可以设计数据库和流程(使用事务、追加只读表和日志系统),以便它们始终能够干净地经受住硬崩溃的考验。
例如,考虑文件系统。在古老的色调过时的时代,当您绊倒电源线时,然后在启动时会出现一个讨厌的事情,系统会执行“完整的磁盘检查”,并且可能会发现许多错误。
但在现代系统中,情况不再如此。随便绊倒电源线吧。您不会得到损坏的文件(除非进程设计得很糟糕,在这种情况下,损坏是应用程序的问题,而不是文件系统的问题),也不需要进行大规模的磁盘检查。
这主要通过一个称为“日志记录”的概念来实现。
假设您想要用文本“Hello, World!”替换一个文件,然后变成“Goodbye now!”。您可以直接开始写入字节。假设您写到了“Goodb, World!”,然后有人绊倒了电缆。
您现在遇到了问题。数据不一致,谁知道发生了什么。
但是想象一下不同的系统:
日志记录
系统首先创建一个名为“.jobrecord”的文件,在其中写入:我将打开此文件,并覆盖开头的数据为“Goodbye, now!”。
然后,它实际上开始执行。
然后,以原子方式删除作业记录(例如通过更新单个字节来标记“完成”)。
现在,在启动时,系统可以检查该文件是否存在,
英文:
As @Turing85's comment hints at, this is extremely dangerous territory: If someone trips over a power cable, your app is out of the running and cannot be started again. permanently. At least, until someone goes in and manually addresses the problem. This is rarely what you want.
The normal solution is to do the locking at the DB level: If it is a 'single file is the database' model, such as H2 or SQLite, then let the DB engine lock the file for writing, and treat the OS-level file lock serve as your gating mechanism. This has the considerable advantage that if app A falls out of the air for any reason (power shortage, hard crash, who knows), the lock is relinquished.
If the DB is a separate running process (psql, mysql, mssql, etc), those have locking features you can use.
If none of those options are available, you can handroll it: You can make files with the new file API that are guaranteed atomic/unique:
int pid = 0; // see below
Path p = Paths.get("/absolute/path/to/agreed/upon/lockfile/location/lockfile.pid");
Files.write(p, String.valueOf(pid), StandardOpenOption.CREATE_NEW);
The CREATE_NEW
open option is asking java to ensure atomicity: Either [A] the file did not exist before, exists now, and it is this process that made it, or [B] this will throw.
There is no [C] this process created it, but another process unluckily was doing the same thing at the same time and also created it and one of these processes is now overwriting the efforts of the other - that is what CREATE_NEW means and guarantees: That this won't happen. (vs the CREATE
option which will overwrite what's there and makes no atomicity guarantees).
You can now use the file system as your global unique lock: To acquire the lock, you make that file. If you can, great. You got it. If you can't, then you have to wait (you'll need to use the watcher API or a loop if you care about acquiring it as soon as you can, not a great option, that is a very expensive operation compared to in-process locking!) - to relinquish the lock, simply delete the file.
To guard against a hardcrash leaving the file there, stuck, permanently, preventing your app from ever running again, it can help to register the 'pid' (process id) inside it. This gives you some debugging if you are manually fixing matters, and you can use to automatically check ('hey, OS, is there even still a process running with id 123981? No? Okay, then it must have hard-crashed and left the lock file in place!'). Unfortunately, working with pids in java is convoluted, as java is more or less designed around the notion that you shouldn't rely too much on the underlying OS, and java does not really assume that 'process IDs' are a thing the underlying OS does. google around for how to obtain it, you CAN do this.
Which gets us to the final point, your evident fear of inconsistency: After all, you actually appear to the desire the clearly insane notion that you want the app to be permanently disabled when there's a hard crash (a process crashes and the lock is not explicitly relinquished). I assume you want this because you are afraid that the database is left in an inconsistent state and you don't want anything to ever touch it again until you manually look at it.
Oookay, well, the lock file business is precisely how you get that. However, this is rather user hostile, and not needed: You can design databases and process flows (using transactions, append-only tables, and journal systems) so that they will always cleanly survive hard crashes.
For example, consider file systems. In ye old aged sepia toned past, when you stumbled over your power cord, then on bootup you'd get a nasty thing where the system would do a 'full disk check', and it may well find a bunch of errors.
But on modern systems this is no longer the case. Trip over that power card all day long. You won't get corrupted files (unless processes are badly designed, in which case the corruption is the fault of the app, not the file system), and no extensive disk checks are needed.
This works primarily by a concept known as 'journalling'.
Say, you want to replace a file that reads "Hello, World!" with the text "Goodbye now!". You could just start writing bytes. Let's say you get to "Goodb, World!" and then someone trips over a cable.
You're now hosed. The data is inconsistent and who knows what was happening.
But imagine a different system:
Journalling
The system first makes a file called '.jobrecord', writes in it: I'm going to open this file, and overwrite the data at the start with 'Goodbye, now!'.
Then, it actually goes ahead and does that.
Then, it deletes the job record in an atomic way (by updating a single byte for example, to mark: "done").
Now, on bootup, the system can check if that file is there, and if it is, check that the job was actually done, or finish it if need be. Voila, now you can never have an inconsistent system.
You can write such tools too.
Alternative: append-only
Another way to roll is that data is only ever added, and has a validity marker. So, you never overwrite any files, you only make new ones, and you 'rotate them into place'. For example, instead of writing over the file, you make a new file called 'target.new', copy over the data, then overwrite the start with "Goodbye, now!", and then atomically rename the file over the original 'target', thus guaranteeing that the original is never harmed, and in one moment in time, the 'view' of the target file is the old one, and in another atomic followup moment, the new one, with never a time in between that is halfway between two points.
A similar concept in databases is to never UPDATE, only INSERT, having an atomically increasing counter, and knowing that 'the current state' is always the row with the highest counter number.
The point is: There are ways to build robust systems that do not ever result in data being inconsistent unless an external force corrupts your data stores.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论