I have a database with the following structure:

Now two paintings can be rated against each other and the rating is then updated. But it can happen that one painting is rated mutliple times at the same time. Therefore i tried using a nested transaction (see below) to avoid race conditions:
function pictureClicked(picture) {
const db = getDatabase();
const leftRef = ref(db, '/paintings/' + generateDbName(currentLeft));
const rightRef = ref(db, '/paintings/' + generateDbName(currentRight));
const winnerLeft = (picture === 'left') ? 1 : 0;
runTransaction(leftRef, (leftP) => {
if (leftP) {
runTransaction(rightRef, (rightP) => {
if (rightP) {
// Calculation of the new rating for each painting (Elo System)
const tRatingLeft = Math.pow(10, leftP.rating / 400);
const tRatingRight = Math.pow(10, rightP.rating / 400);
const expectedLeft = tRatingLeft / (tRatingLeft + tRatingRight);
const expectedRight = tRatingRight / (tRatingLeft + tRatingRight);
// Setting new rating
leftP.rating = leftP.rating + 32 * (winnerLeft - expectedLeft);
rightP.rating = rightP.rating + 32 * ((1 - winnerLeft) - expectedRight);
}
return rightP;
});
}
return leftP;
});
}
Now this solution doesn't really work and I have read that it isn't the best approach to this problem. The only solution I found was running a transaction on the root of my database but I'm afraid that this would lead to a bottleneck and many of the launched transactions wouldn't even write to the database since transactions are only retried 25 times (or is my fear here unfounded?). Are there any other solutions to this problem?
Firebase Realtime Database transactions run against a single path in your database. There is currently no way to run a transaction against multiple path, so the common approach is to run the transaction against the lowest-level shared path (/paintings in your case).
This can indeed lead to more contention as you have more users performing concurrent transactions.
The only alternative I've used at times is to build my own transaction mechanism using:
This is fairly non-trivial even for simple cases, and may require changing your data structure and/or adding additional values to each request. But if you get past those hurdles, it will allow you to run a "transaction" across multiple paths.