The interesting part, and the part where all of the difficulties lie, is in wrapping things up in transactions.
You ll need two tables: A table of available work, and a table recording work that is in progress. The "work in progress" table has a unique foreign key to the work-available table.
A process wishing to do work first locates a row from the table of work to be done. This should be done using a random sort order, in order to reduce contention.
That process the removes the "work in progress" row. It was never meant to be persist outside the transaction. It s only for locking.
That process then starts a transaction.
That process then creates a row in the "work in progress" table, with a foreign key referencing the work that is being done. It should then do the work. As a part of doing that work, it should change the state of the item being worked on (e.g., making it "finished" and no longer available to be worked on).
The process the commits its transaction.
If some other process has grabbed the work, then this process s transaction will fail due to its attempt to commit a duplicate foreign key to the "work in progress" table. In that case, the process should back off for a short, random interval, and to back to the start, trying to locate some work to do.
Monitor the "work in progress" table carefully. Some databases, or some versions of some database, don t expect a table such work-in-progress table to be used as a queue, with rows constantly being created and deleted. Specifically, older versions of Postgresql had difficulty cleaning up the old, no longer used rows, causing table bloat and poor performance.