DataStores - Beginners to Advanced

Hey!
This tutorial will teach you about regular DataStores (I won’t include OrderedDataStores, but they are pretty similar). DataStore Version 2.0 won’t be included in this tutorial either.
Over the course of this, we will script a DataStore (that will hopefully be safe).

Important:

This won’t be as safe as DataStore2 or ProfileService, as both are heavily tested. So just use them if you need a safe DataStore, I only made this for showing you some ideas that can be applied to DataStores. Bugs in this are possible.

Here a summary:

What are DataStores?

GetAsync
SetAsync
Why is our data not saving??
BindToClose

How do I make my DataStore safer?

Pcalls
Retries
UpdateAsync > SetAsync
Autosaving
BindToClose, continuation
DataStore Corruption
Berezaa’s Method
SessionLocking

We will start with “What are DataStores”.
DataStores are “containers” that you can store data in. They are shared per game, so different places can access and change the same data. You can imagine them as dictionaries.

GetAsync
With GetAsync, you can get the data with a key that you have to pass as the first argument (arguments = the things you pass when calling a function, parameters = the things you receive inside of the function).

So let’s start scripting our DataStore!
Before we use anything, we have to first create it. We use GetDataStore for that.
(btw, I will use ColdCode to screenshot the code I wrote in VSC instead of creating it here, I think that looks better)

Fun fact: you can pass 3 arguments to GetDataStore, the name, the scope and options. Options is just for the upcoming DataStore 1.1 API though, we will only use the name.

Now that we have created our DataStore, we will define Players as variable and make a function for the PlayerAdded event that we will later connect to it.

Our key will be “Player_” and the userId concatenated to that (you can also use the userId by itself).
Pretty easy right? Just GetAsync on our DataStore and use that data or 0 (in case it’s nil because the player is here for the first time).

You might find it weird how I’m parenting the stuff at the end, it’s because GetAsync can yield. If I parented leaderstats to player right after creating it, how would we know if the player’s cash is 0 because the GetAsync is still trying to get his data or because he is new?
That’s why I parent the leaderstats to the player as last thing, now we can simply use player:WaitForChild(“leaderstats”) and from there on we know that everything is loaded.

Now we just connect that function to the PlayerAdded event and we are done, right?

Not really, we still need to make it save, so also make a save function.

SetAsync

With SetAsync, we set a key to something. I think the easiest way to imagine this is with dictionaries.
We have a dictionary, the keys consist of “Player_” … player.UserId and the values are the cash of the players.

image

We need 2 arguments for SetAsync: the key and the value.
Kinda like how in a dictionary we would do

So let’s script the function and connect it to PlayerRemoving!

We are using FindFirstChild on the leaderstats because if the player joined and leaved immediately, maybe the GetAsync would still try to get his data - meaning leaderstats isn’t parented yet. Only save something if his leaderstats is loaded (also, we don’t really have time to wait for it, he is leaving).

Alright. That wasn’t hard, was it?
Here is the full code we have:

Why is our data not saving??

Now if we insert that into studio though, you will notice a problem (actually two if you don’t have API Services enabled :laughing:): it’s not saving!

Huh? Why is that? Did we do something wrong?
No, we didn’t! Many scripters that are newer to DataStores face this issue.
In fact, I think this is the most common one.
There is just something we didn’t add yet: a BindToClose.

BindToClose

Now, you might wonder: why do I need a BindToClose? We already have a PlayerRemoving event that has a saving function connected to it!

Well, yes, but often, Studio simply shuts down too fast making it impossible for PlayerRemoving to fire.
So what do we do?
→ We will use BindToClose to delay the shutdown.

image

Now, magically, it works!
So are we done?
→ No, we are not. This DataStore is not safe at all.

How do I make my DataStore safer?

Alright, we are halfway through this tutorial right now. We got the basics down, we just have to improve what we have. This will start becoming a little harder now (don’t worry though, not that hard!).

Have you ever heard of DataStore outages? If you don’t know what those are, they are basically situations, where the DataStores just don’t work: DataStoreService is down.

Now even if there is a DataStore outage or SetAsync, GetAsync, … fail, you want to keep the data-loss as small as possible.

Pcalls

I think the most important thing is to prevent your code from breaking in case SetAsync / GetAsync / UpdateAsync … fails. → You can do this with a pcall!

Pcalls are basically protected calls that catch errors. They return two values: if it was a success and if there were any returns (stuff that you return inside of the pcall or errormessages).

Let’s implement them!
First, for our Get-function.

success becomes a boolean (true/false) and ret becomes our returned value (the cash or in case of a fail the errormessage).

Now, look at the pcall. Doesn’t it look bad how it just calls a function to call another function?
The anonymous function we pass to pcall does nothing but call GetAsync and return that value!

Well, we can fix that! Instead of the anonymous function, we will right away pass GetAsync.
How do we do that? Like this: pcall(dataStore:GetAsync(key))? No!

First, we have to pass the function → dataStore.GetAsync.
Next, we have to pass self (what we want to call it on) → dataStore.
Now, we just have to pass any arguments that we want to give to it. → key.

I understand if you find this kinda confusing, you just have to know that you first pass the function, then self (the thing you call GetAsync on) and then your arguments.

This will automatically return the result of GetAsync and make ret become that.

Now, how do we continue? We will simply write an if-statement. If it was a success, then make cash become the returned value, if it wasn’t, then print the errormessage.

Let’s do the same for SetAsync!

First the function, then self, then the arguments.
You can also exponentially wait on failure, thanks to Disgusted by You#8532 for telling me about this.

Are we done now?
Not really. Imagine one of these calls fail. The player won’t really care about the errormessage, he will care about his data not loading/saving. So what can we do?

Retries

We can retry if it was not a success. Maybe next time, it will be a success!
I like to use repeat until loops for this, I’ve seen some people use while loops too, that works aswell.


Let’s start with our Get function. Now there is two ways people do this:

Some like to have a variable that counts the retries. If it reached a certain number, then stop retrying, as it’s probably an outage.

or…

just leave out the retries variable and retry until its a success.

If I had to choose I would pick the second option and implement an if-statement that breaks the loop if the player has left. So let’s do that.

We will add a function that yields if there isn’t enough budget.

Thanks to @colbert2677 for suggesting me to use this in one of my posts!

Here is the function, slightly modified as we don’t need two parameters, we will only want one request (originally it had a requestAmount parameter too).

Let’s use that function in our repeat loop!

We pass Enum.DataStoreRequestType.GetAsync as that’s our requestType - should be self-explanatory.

Now, we don’t even need the else statement, it will retry until it’s a success or the player has left.

Now, as you can see, success is underlined!

That can be fixed pretty easily, it’s simply because of scope - the scope of success is inside of the repeat until loop and we are trying to use it outside. Simply define success, ret before.

Now, let’s do the same for SetAsync:

just that here, we won’t stop if the player has left. We will instead continue trying to save - we have all the stuff we need stored in variables. If you are wondering about why I used SetIncrementAsync: it refers to both SetAsync and IncrementAsync.

UpdateAsync > SetAsync

Alright, now our DataStore is already safer than before but we want to make it even safer!
We will switch from SetAsync to UpdateAsync.

UpdateAsync can be pretty confusing at first, especially with that function you have to pass.


UpdateAsync basically takes the key as first parameter - just like SetAsync but a function as second parameter.

Why is that?

Basically, UpdateAsync will give us an “oldData” parameter in the function that we can use.
We just have to return what it should save. → Now it makes more sense, right?

UpdateAsync also ensures that no data is overwritten when another game server updated the key in the short timespan between retrieving the key’s current value and setting the key’s value. → The function will get called again!

So let’s switch from SetAsync to UpdateAsync!

I’m not using UpdateAsync because of the oldData parameter it gives, but because of the reason I stated above.

Autosaving

Now you might be wondering why you need to autosave even though we made this “super safe” PlayerRemoving event. Even though if there is a DataStore outage and the player leaves, it will retry until DataStores are back up again, the server might still shutdown.

In that case, the player’s entire session would be lost VS if we used Autosaving and only a part of the player’s session would be lost.

So let’s implement something like that!

We can right away wrap it in a coroutine - the getRequestBudget function does the waiting if there is no more budget for it for us. When wrapping a function into a coroutine, it returns another function. You can call that and pass the arguments.

Why are you using ipairs?

ipairs is slightly faster than pairs and should be used for arrays.

Why are you using GetPlayers

If you accidentally put a part into the Players service, using GetChildren would try to call save on that part.

A quick note
What if a player joined while we were making these functions? → Because of that, let’s iterate through the Players service and call setUp on every player before connecting our functions with a coroutine.

BindToClose, continuation

Alright, you might be wondering why we need a continuation, isn’t our BindToClose done already?
Well, it looks like this:

image

This isn’t really how a BindToClose should look like, we just used this so it delays the shutdown in Studio - giving PlayerRemoving time to fire. But what if we aren’t in Studio? → We should save everyone’s data then, Roblox gives us 30 seconds. After those 30 seconds, the game will shut down regardless of our function not being done executing.

I won’t implement any cooldown, even if it adds stuff to the queue.

Now how do we make it wait on PlayerRemoving but not on Shutdown? → We can use an extra parameter!

This might sound confusing but after you’ve seen the code I think it should be clear.

See how I added a new parameter called “dontWait”? If that’s true, then it will not wait.
We will only make it true when the game is shutting down.

Now, we just need a way to check if we are in Studio or not. If we are, then simply wait(1) like we did before, if not, save everyone’s data without waiting. → We can use RunService:IsStudio() for this.

Define RunService at the top and replace our onShutdown function with this:

We are pretty much done with our regular DataStore.

Fun fact: This is the second day of writing this tutorial and I forgot to save the script yesterday and have to rewrite it now. F in the chat for me.

I think if you kept it like this it should be safe already, but what do we do to make it even safer?
→ There is something called DataStore Corruption

It should be pretty rare and as far as I know it shouldn’t occur because of Roblox but because of but to be safe we are still going to handle that.
Now, this will get a little harder: we are going to use Berezaa’s method!

UPDATE: NOW THAT DATASTORE V2.0 IS OUT, THIS IS PROBABLY DEPRECATED, YOU CAN ACHIEVE THE SAME THING USING LISTVERSIONASYNC.

YOU CAN SKIP TO SESSIONLOCKING.

After reading this, you might be a little confused but it’s not that hard, don’t worry.

So basically he is saying that you could have two DataStores for every player: a regular DataStore and an OrderedDataStore. The OrderedDataStore will save the keys of our regular DataStore and our regular DataStore will have versions of data from the player.

First, we will want to make a safeCall function, something that does all these repeat until and pcalls for us, as we will need it for more things now, not only GetAsync and UpdateAsync but GetSortedAsync and the OrderedDataStore version of SetAsync too! If we don’t do that, our code will look like a mess (Don’t-Repeat-Yourself principle).

We will want to make this function very flexible, so that we can use it for basically anything regarding DataStores. Now how do we do that? → We will be using an if-statement to check if an argument exists and the “and” operator.

Let’s do that!

I understand that this might look really confusing to you. I will cover the things I think a newer scripter wouldn’t understand here:

the “…” → Basically, this means “everything followed after that”.
Let me give you an example:

Alright, so when we call doSomething, we pass “hi”, “idk” and “what” into doSomething as arguments.
It receives those in a tuple. What does that mean? Remember how we define success, ret in a pcall?
Pcall basically gives back a tuple!

We can do something similar inside of the function.

Not that hard, right?

Alright, now the next thing that might be hard to understand for a newer scripter is the

I put these two conditions into brackets so it’s easier to look at.
Basically, this is saying that it should stop whe the pcall is a success or when we have given a playerName argument when calling it and it can’t find that playerName string in the Players service (meaning the player has left).

Hm, is there anything else? I don’t think so, so let’s move on.

We can use that function now for basically anything without having to write the repeat loops, so let’s do that for now.

For our setUp function it will look like this:


(damn, what a long line!)

And for our save function like this:

We don’t do “local success, ret =” because we don’t really care about the return, it will try until it has saved successfully.

the (not dontWait and Enum.DataStoreRequestType.UpdateAsync) might confuse you, basically it means that if no dontWait argument was given, it will execute the waitForRequestBudget function with the Enum.DataStoreRequestType.UpdateAsync argument (not nil and object = true and object = object), if the dontWait is true it will become (not true and object = nil and object = nil).

Alright, that’s it! Now we can get into implementing Berezaa’s method!

First, remove the local dataStore = DataStoreService:GetDataStore("Name") line at the top, every player will have their own dataStore.

Now, inside of the setUp function, we will define two new variables:

Remember, the orderedDataStore holds the keys for our latest versions and our normal DataStore will use that key from the orderedDataStore to get our data. → If our data is corrupted, we can go back one version, if that data is not corrupted, then use that version → better than resetting his stats.

Now, how do we determine if data is corrupted or not? → The GetAsync will error with the errorcode 501 or 504, so we can simply use string.find/match on it.
As far as I know, Roblox can also return false values; for example a string instead of our data, that’s why some people check the type. We are going to do that too.

If 501 or 504 are found in the errormessage, stop the entire function, return nil, we don’t want to retry.

Now, in our setUp function it’s going to be a little more complicated: we get the two DataStores, safeCall GetSortedAsync and get the current page. Then, we check if the current page contains more than 0 elements (if it’s empty it means the player is new), and if so, loop through it, check if the player is still there, if not, stop the function. safeCall GetAsync on our regular DataStore and get the success and ret. If it’s a success (meaning the player didn’t leave after retrying and the data wasn’t corrupted, then set his cash to the ret, parent cash to leaderstats and leaderstats to player and break out of the loop.

Now if the currentPage contains no more than 0 elements, we just set the cash to 0 and parent it and leaderstats.

Note that this would only check the first 100 elements, you could implement a pages:AdvanceToNextPageAsync() to make it so it can use every element, I won’t be doing that as that would make it probably even more confusing.

So basically, it first gets all the pages with 100 elements on each page, then gets the current page (first one), checks if it has more than 0 elements in it and then iterates through it, checking if the player is still there, setting the dataStoreKey to dataStoreKey.value (GetSortedAsync gives stuff back in dictionaries that have a key and a value, we are only interested in the value (it will be our dataStoreKey).

safeCall GetAsync on our dataStore with that key and if it’s a success, then set the cashValue to the returned value, if it’s not then it will simply continue iterating; it can only not be a success if the player has left or if it’s corrupted. At the beginning we have an if statement that stops the function if the player has left, so it will only try with an older version if it was corrupted.

Now, let’s get to our save function. This one will be shorter.

The only thing we have to do is get the latest key and add 1 to that, save that into our OrderedDataStore and use it as key for our regular DataStore to save the player’s new data.

Alright. Let’s test this now. Play → set our cash to 500 on the server, leave, play again and boom it works!

Now let’s test the corruption handling. I set my cashValue to 80, left, then 90, left and added this to our safecall to simulate a corruption:

It worked perfectly for me, it has gone back to 80.

SessionLocking

Alright, now this is even more important than Corruption Handling in my opinion, as Corruption occurs very rarely as far as I know.

Imagine following scenario: your game has a trading system. Player1 trades with his friend, Player2, and gives him his diamond sword. Now, Player1 quickly leaves. Our save function takes a bit longer than usualy to save (UpdateAsync fails a couple of times for example). Now, in that time Player1 rejoins and gets his old diamond sword that he just traded away back. Why? Because our DataStore still didn’t save his new data, our GetAsync got his old one. Now, Player1 and Player2 both have diamond swords → they discovered a method to dupe. Now even if our DataStore finally saved, when Player1 leaves, it will save his old diamond sword that he got when rejoining.

So what can we do to stop that?
→ We could set a value in their data to true when they join and to false when saving. In our PlayerAdded event, we can check if that value is true and if so, kick them or retry until it’s false, the DataStore still didn’t save.

@ArtFoundation did a good job at explaining this here: Session locking explained (Datastore)

So let’s try making something like that, to make this less complicated, I won’t be using our code that handles corruptions (I don’t even know if you could implement SessionLocking into there, it saves a new version everytimes, it would be tricky probably), but our old one that simply retries. I also won’t be using our safeCall function. I will remove the requestType parameter from waitForRequestBudget as we will only use Enum.DataStoreRequestType.UpdateAsync.

This will be easier than Berezaa’s method. Let’s start with our Get function:

Basically we are checking if the oldData is SessionLocked, and if so, set the shouldWait to true → it will wait 5 seconds and retry. If not, we will set it to true, put our oldData into our data variable (we replaced an additional GetAsync like this) → save the data we set the SessionLock to true.

Now, we just check if it was a success (if not, that means the player has left) and the data is there, set the cash to the data-table’s cash and parent everything.

For our Save function we have two options:
it’s autosaving, which means we should keep the SessionLock true,
the player is leaving, which means we should make the SessionLock false.

So I added an extra parameter called “dontLeave”. This will be set to nil if the player is leaving and to true when we autosave.

shouldLock basically says “if dontLeave is true then make it true, else make it false”

Because of that, we have to change our autosave and leave functions so they pass the correct arguments:


And we should be done.

Now, the only problem is that if the game somehow shuts down and not everyone’s SessionLock could be set to nil, the player is going to be banned.

In order to fix that, we can use an os.time() value as SessionLock instead and declare a session as dead if the current time minus that time is more than 30 minutes.

We will change our setUp function to:

Basically, we set ret to “Wait” if the session is not dead yet, and if it’s dead, we just take over the session like if there was no SessionLock at all.

In our save function, we only have to change one line:

image

to

So at this point we are pretty much done with this tutorial, I’m going to give you the code for the Session-Locking DataStore:

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local dataStore = DataStoreService:GetDataStore("Players")
local default = {
	SessionLock = false,
	Cash = 0
}
local updateAsync = Enum.DataStoreRequestType.UpdateAsync

local function waitForRequestBudget()
    local currentBudget = DataStoreService:GetRequestBudgetForRequestType(updateAsync)

    while currentBudget < 1 do
        currentBudget = DataStoreService:GetRequestBudgetForRequestType(updateAsync)
        wait(5)
    end
end

local function setUp(player)
	local name = player.Name
	local userId = player.UserId
	local key = "Player_" .. userId

	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"

	local cash = Instance.new("IntValue")
	cash.Name = "Cash"

	local success, data, shouldWait
	repeat
		waitForRequestBudget()
		success = pcall(dataStore.UpdateAsync, dataStore, key, function(oldData)
			oldData = oldData or default
			if oldData.SessionLock then
				--He is still sessionlocked, so just wait
				if os.time() - oldData.SessionLock < 1800 then
					--session is alive
					shouldWait = true
				else
					--session is dead, take over
					oldData.SessionLock = os.time()
					data = oldData
					return data
				end
			else
				oldData.SessionLock = os.time()
				data = oldData
				return data
			end
		end)

		if shouldWait then
			task.wait(5)
			shouldWait = false
		end
	until (success and data) or not Players:FindFirstChild(name)
	if success and data then
		cash.Value = data.Cash

		cash.Parent = leaderstats
		leaderstats.Parent = player
	end
end

local function save(player, dontLeave, dontWait)
    local userId = player.UserId
    local key = "Player_" .. userId

    local leaderstats = player:FindFirstChild("leaderstats")

    if leaderstats then
        local cashValue = leaderstats.Cash.Value
        local success

        repeat
            if not dontWait then
                waitForRequestBudget()
            end
            success = pcall(dataStore.UpdateAsync, dataStore, key, function()
                return {
                    SessionLock = dontLeave and os.time() or nil,
                    Cash = cashValue
                }
            end)
        until success
    end
end

local function onShutdown()
	if RunService:IsStudio() then
		task.wait(2)
	else
		local finished = Instance.new("BindableEvent")
		local allPlayers = Players:GetPlayers()
		local leftPlayers = #allPlayers

		for _,player in ipairs(allPlayers) do
			coroutine.wrap(function()
				save(player, nil, true)
				leftPlayers -= 1
				if leftPlayers == 0 then
					finished:Fire()
				end
			end)()
		end

		finished.Event:Wait()
	end
end

for _,player in ipairs(Players:GetPlayers()) do
    coroutine.wrap(setUp)(player)
end

Players.PlayerAdded:Connect(setUp)
Players.PlayerRemoving:Connect(save)
game:BindToClose(onShutdown)

while true do
    wait(60)
    for _,player in ipairs(Players:GetPlayers()) do
        coroutine.wrap(save)(player, true)
    end
end

and here for the Berezaa’s method one:

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local function waitForRequestBudget(requestType)
	local currentBudget = DataStoreService:GetRequestBudgetForRequestType(requestType)

	while currentBudget < 1 do
		currentBudget = DataStoreService:GetRequestBudgetForRequestType(requestType)
		wait(5)
	end
end

local function safeCall(playerName, func, self, requestType, ...)
	local success, ret
	
	repeat
		if requestType then
			waitForRequestBudget(requestType) 
		end
		success, ret = pcall(func, self, ...)

		if not success then
			print("Error: " .. ret)
			if string.find(ret, "501") or string.find(ret, "504") then
				return
			end
		end
	until (success) or (playerName and not Players:FindFirstChild(playerName))

	return success, ret
end

local function setUp(player)
	local name = player.Name
	local userId = player.UserId
	local key = "Player_" .. userId

	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"

	local cash = Instance.new("IntValue")
	cash.Name = "Cash"

	local dataStore = DataStoreService:GetDataStore(key)
	local orderedDataStore = DataStoreService:GetOrderedDataStore(key)

	local _, pages = safeCall(name, orderedDataStore.GetSortedAsync, orderedDataStore, Enum.DataStoreRequestType.GetSortedAsync, false, 100)
	local currentPage = pages:GetCurrentPage()

	if #currentPage > 0 then
		for _, dataStoreKey in ipairs(currentPage) do
			if not Players:FindFirstChild(name) then return end
			dataStoreKey = dataStoreKey.value
			local success, ret = safeCall(name, dataStore.GetAsync, dataStore, Enum.DataStoreRequestType.GetAsync, dataStoreKey)

			if success then
				cash.Value = ret
				cash.Parent = leaderstats
				leaderstats.Parent = player
				break
			end
		end
	else
		cash.Value = 0
		cash.Parent = leaderstats
		leaderstats.Parent = player
	end
end

local function save(player, dontWait)
	local userId = player.UserId
	local key = "Player_" .. userId
	local leaderstats = player:FindFirstChild("leaderstats")

	if leaderstats then
		local cashValue = leaderstats.Cash.Value

		local dataStore = DataStoreService:GetDataStore(key)
		local orderedDataStore = DataStoreService:GetOrderedDataStore(key)

		local _, pages = safeCall(nil, orderedDataStore.GetSortedAsync, orderedDataStore, Enum.DataStoreRequestType.GetSortedAsync, false, 1)
		local latest = pages:GetCurrentPage()[1] or 0
		local should = (type(latest) == "table" and latest.value or 0) + 1


		safeCall(nil, orderedDataStore.UpdateAsync, orderedDataStore, (not dontWait and Enum.DataStoreRequestType.UpdateAsync), should, function()
			return should
		end)
		safeCall(nil, dataStore.UpdateAsync, dataStore, (not dontWait and Enum.DataStoreRequestType.UpdateAsync), should, function()
			return cashValue
		end)
	end
end

local function onShutdown()
	if RunService:IsStudio() then
		task.wait(2)
	else
		local finished = Instance.new("BindableEvent")
		local allPlayers = Players:GetPlayers()
		local leftPlayers = #allPlayers

		for _,player in ipairs(allPlayers) do
			coroutine.wrap(function()
				save(player, true)
				leftPlayers -= 1
				if leftPlayers == 0 then
					finished:Fire()
				end
			end)()
		end

		finished.Event:Wait()
	end
end

for _, player in ipairs(Players:GetPlayers()) do
	coroutine.wrap(setUp)(player)
end

Players.PlayerAdded:Connect(setUp)
Players.PlayerRemoving:Connect(save)
game:BindToClose(onShutdown)

while true do
	wait(60)
	for _, player in ipairs(Players:GetPlayers()) do
		coroutine.wrap(save)(player)
	end
end

Update: Now that the task library is out, use task.wait() instead of wait().

I hope you liked it!
It’s possible that some things in this tutorial are wrong or there are bugs, if you find something, please tell me.
If you see anything wrong in this / anything missing, just reply to this tutorial.

I, myself, most of the time just use ProfileService.
ProfileService and DataStore2 are both heavily tested so probably a lot safer than this.

245 Likes

I tried to understand its my fifth time and my brain cant handle it i need help :sob:

8 Likes

What exactly do you not understand?

4 Likes

This tutorial might as well be one of the best I have ever seen. Just at a glance, it looks very nice and professional / easy to understand. Amazing job, this is what tutorials should be.


14 Likes

Out of the two finished scripts you gave, which would be best in a production setting?

3 Likes

I’m not sure, as far as I know, the two most popular DataStore modules (DataStore2 and ProfileService) use different ways: DataStore2 uses Ordered Backups by default which is basically Berezaa’s Method and ProfileService uses SessionLocking.

I think SessionLocking is more useful, as corruption should be pretty rare.

You could probably also implement both, when saving a new version set it’s SessionLock to os.time(), when joining, check the latest version’s SessionLock.

1 Like

Everything im trying to understand but its hard Do u have a simple way

1 Like

I tried to explain it the easiest way I can, if you tell me what exactly you don’t understand I could give you more details about it.

1 Like

A good method is also to use two datastores for one player, one used as the default to save the data and an OrderedDataStore to save the keys (os.time()) and receive the data from the main datastore using the most recent key page.

1 Like

That’s what I’m doing in the Berezaa’s Method script except I increment the last key with 1 and use that instead of os.time(), I’ve heard that there were some problems with os.time().

2 Likes

The pages and the Session lock

1 Like

The pages (Berezaa’s Method) are much harder than SessionLocking in my opinion (I don’t know why I explained Berezaa’s Method before SessionLocking in the tutorial), so let’s begin with SessionLocking:


You have to first know why you want SessionLocking at all.

After you’ve read that, you probably know the reason why you need something that stops the data from getting when the old one is not saved yet, you just don’t know how to make one.

Join → Is the saved data’s sessionlock false?
→ Not false? Wait until it is false!
→ false? Override it with true (later with os.time()), we don’t want a new server to claim our session! We want the new server to know that the player is already playing, the sessionlock is true!

Leave (same as shutdown)
→ Set the sessionlock to false (later os.time()).

AutoSave
→ Don’t set the sessionlock to anything, just save the data, the player is still playing!

Just try to visualize in your brain how this will work, when joining, it will set the sessionlock to true, when leaving, it will set it to false.
If the sessionlock is already true when the player joins, it obviously means that the last server somehow couldn’t set it to nil yet.

Now if you’ve understood that, you’ve already pretty much learned SessionLocking, but there is only one more thing you should do: use os.time() instead of true/false.

Imagine: the server shuts down and the 30 seconds Roblox gives us with BindToClose aren’t enough - some player’s data won’t be saved (which isn’t that dramatic since we have autosaving).
What is dramatic though is that the sessionlock will never be set to nil! It will stay true.

So what do we do? → Use os.time() instead of true/false so we can check if more than I would say 30 minutes have passed, that should be a good amount. If more than 30 minutes have passed, that means something is wrong, it’s impossible that the server still couldn’t save even though so much time has passed. In that case, we can just take over the session, just like if it was false, like if everything was correct and there were no complications.


Now to the pages, Berezaa’s Method.
We will use a similar approach, first, understand why we need it:

Data might be corrupted if Roblox returns us an errormessage with the error-code 501 or 504. We can’t get his newest data anymore, it’s corrupted. We want to get an older version, because that’s the best way to lower the data-loss as much as possible, right?
Just give him his data the autosave saved like 40 seconds ago.

So that is Berezaa’s Method pretty much, make every player get his own DataStore so we can save the versions as keys.

DataStore = {
[1] = 0, --He started with 0 cash,
[2] = 50, --then, after 40 seconds, the autosave saved again, creating a new version, by the time he has 50 cash
[3] = 20 --next autosave saves and he lost 30 cash, probably because he bought something!
}

^ Visualization of our DataStore

Now, we just need to get the latest key, right? When he next joins, we want to somehow know what is the latest key, in our Visualization it’s “DataVersion3”. It’s the most recent one.

So how do we do that? → By saving our keys into an OrderedDataStore. We then can use GetSortedAsync and it will return the largest indexes, which are our most recent keys.

Visualization:

OrderedDataStore = {
[1] = 1, --here he started the game and the autosave saved for the first time
[2] = 2, --here, autosave saved for the second time
[3] = 3 --let's say he left here
}

Now, with OrderedDataStore:GetSortedAsync, it would return something like this:

OrderedDataStore = {
[1] = 3,
[2] = 2,
[3] = 1
}

See what it’s doing? It’s taking the largest values first (ascending). We can simply use table[1] and it will give us our latest key!

(Note that these examples are simplified, in actuality, OrderedDataStores don’t simply save the version like that, they save two objects into a table: key and value, what I’m showing you with the version are the values)

We can just use our latest key for our DataStore and boom, we got his latest data!

Now, when saving, we get the latest version using our method above and then we add one to that, so if it was 3, it now becomes 4.
We save 4 to our OrderedDataStore and use it as key for our normal DataStore to save the data.
Next time the player joins, we simply use the GetSortedAsync again, get the first key which has the highest value, and it will be 4.

Now, just check if the DataStore returns an error with the error-code 501 or 504 and if so, just go to the second element. Instead of picking the latest one, pick the second latest one. If the second latest one is also corrupted, pick the third latest one etc. - that’s why we use a for loop.


Berezaa’s Method is much more complicated than SessionLocking in my opinion, I feel you if you are having a hard time understanding this.

15 Likes

Holy thank you so much for this whole thing you made for me I really thank you from my heart <3

2 Likes

If you still have questions, just ask here, I’m not an expert - that’s why I said it’s probably better to just use ProfileService/DataStore2, both are tested and you can probably feel safer with them, but I will try helping if I can.

3 Likes

What does the waitForRequestBudget function do in the script?

2 Likes

Basically just waits until there is enough budget for requests, with DataStores you can’t just save a thousand times a second, there is a limit.
https://developer.roblox.com/en-us/articles/Datastore-Errors

2 Likes

If you make frequent requests it would lead to throttling and cause players to lose data?

3 Likes

It would first put the requests into a queue, if that queue fills up (30 requests), then further requests will be dropped as far as I know.

1 Like

Or… You can use DataStore2. Fast, Better, Easy

1 Like

Mentioned that in the tutorial twice, but you won’t really learn anything from just using DS2/ProfileService.

2 Likes