I built and manage an application which is slowly getting a lot of traction. One particular piece of the application is turning into a performance bottle neck, so I'm trying to find out if there is a better or more perfomant way to solve my issue.
The particular piece of code is responsible for turning a string into a series of sections. For example: this/is/a/section would turn into 4 sections. IF they don't allready exist. If they do exsist then it will just return the existing sections. The mentioned example would tun in the following 4 sections:
[id => 1, name => "this", parent_section_id => null],
[id => 2, name => "is", parent_section_id => 1],
[id => 3, name => "a", parent_section_id => 2],
[id => 4, name => "section", parent_section_id => 3]
To accomplish this, I use the following code:
$path = "this/is/a/section";
collect(explode('/', $path))
->reduce(function ($parentSection, $sectionName) {
return Section::firstOrCreate([
'name' => $sectionName,
'parent_section_id' => $parentSection ? $parentSection->id : null
]);
}, null);
The race condition I ran into is that if there are multiple proccess running at the same time, it sometimes happened that a section was created twice. This is because the firstOrCreate does two queries: one to check if the section exists, and another to create it if it doesn't. If another process would run the same code just after the first query was run, they both would "think" there wasn't an exsisting section already, which resulted in the section being created twice.
To solve this, I'm currently using an atomic lock:
Cache::lock('SECTION_CREATION_LOCK', 10)->block(5, function () {
// the code block mentioned earlier will be run inside this lock.
});
If i'm not mistaken, this will try for 5 seconds to create a (max) 10 second lock. During this lock, all other processes will wait until the lock is released.
Unfortunately this is a huge performance bottleneck which also causes a lot of lock exceptions due to the timeout.
Is there any cleaner way to prevent the race condition? Or is there a better way to use the locking mechanism? Any help is highly appriciated.