0% found this document useful (0 votes)
10 views6 pages

MMO Database Optimization Techniques

Uploaded by

cezarjont
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
10 views6 pages

MMO Database Optimization Techniques

Uploaded by

cezarjont
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

This is Part 3 of 3 of the technical specification for "Synchronizing Reality.

"

In this final section, we address the critical "backend of the backend." While the game loop handles
milliseconds, the database must handle years. We will explore how to persist high-frequency data (like HP
changing 20 times a second) without melting a standard SQL database, and how to structure inventory
systems to prevent the most dangerous exploit in MMO history: The Item Dupe.

Part 3: Persistence, Security, and Scale


Chapter XI: The Database Bottleneck
The "Write-Intensity" Problem
In a standard web app, a user might update their profile once a day. In Tantra Genesis M, a player in
combat might change their HP, Mana, Position, and Cooldown timers 10 times per second.

If you have 5,000 active users, and you attempt to write every state change directly to a relational database
(PostgreSQL/MySQL):

5,000 players * 10 updates/sec = 50,000 INSERT/UPDATE queries per second.

This will crash almost any standard SQL server immediately. The disk I/O cannot keep up. Furthermore,
database transactions are "blocking." If the Game Loop waits for the Database to confirm "HP Saved"
before proceeding to the next frame, the entire server will freeze (lag) whenever the database hiccups.

The Solution: The Write-Back Cache Pattern


We must separate Hot Data (RAM) from Cold Data (Disk).

1.​ The Hot Layer (Redis): All real-time data lives in memory. When a player takes damage, we
update a Redis Key or a C# variable in RAM. This takes nanoseconds.
2.​ The Flush Worker (The Persistor): A separate thread or service runs in the background. Every N
minutes (or on player logout), it dumps the current state from RAM to the SQL Database.

This reduces 50,000 writes/sec to effectively 0 writes/sec during gameplay, with a burst of writes only upon
logout or periodic autosave.

Chapter XII: Designing the Schema (JSON vs. Relational)


The Inventory Problem
How do you store an item in an RPG?

●​ Old School: A rigid SQL table.


○​ Table: Items (ID, Name, Damage, StrengthBonus, AgilityBonus...)
○​ Problem: What if you add a new stat "Life Steal"? You have to alter the table structure for
millions of rows.
●​ The Modern Approach: Hybrid JSON (NoSQL within SQL).

We use a Relational Database (PostgreSQL) for account safety (ACID compliance), but we use a JSONB
column for the item attributes. This allows for flexible, procedural loot generation without schema
migrations.

Python Implementation: Async Database Worker


Here is how we implement the "Write-Back" pattern using Python's asyncio to ensure the game loop never
blocks.
Python
import asyncio
import json
import asyncpg # Async PostgreSQL driver
import redis

class PersistenceManager:
def __init__(self):
# Redis for HOT data (The "Truth" during gameplay)
[Link] = [Link](host='localhost', port=6379, db=0)
self.db_pool = None

# Dirty set: Track which players need saving


self.dirty_players = set()

async def connect_db(self):


self.db_pool = await asyncpg.create_pool(user='admin', password='password', database='tantra_m')

def update_player_cache(self, player_id, data):


"""
Called by Game Loop. Fast. Non-blocking.
"""
# 1. Update RAM (Redis)
[Link](f"player:{player_id}", [Link](data))

# 2. Mark as Dirty
self.dirty_players.add(player_id)

async def background_save_loop(self):


"""
Runs continuously in a separate thread/task.
"""
while True:
await [Link](60) # Autosave every 60 seconds
if self.dirty_players:
await self.flush_to_sql()

async def flush_to_sql(self):


print(f"Persisting {len(self.dirty_players)} players to Disk...")

async with self.db_pool.acquire() as conn:


async with [Link]():
for player_id in list(self.dirty_players):
# 1. Fetch latest state from Redis
raw_data = [Link](f"player:{player_id}")
if raw_data:
player_data = [Link](raw_data)

# 2. SQL Upsert (Update if exists, Insert if new)


await [Link]('''
INSERT INTO characters (id, name, level, inventory_json, position_x, position_y)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (id) DO UPDATE
SET level=$3, inventory_json=$4, position_x=$5, position_y=$6
''', player_id, player_data['name'], player_data['lvl'],
[Link](player_data['inventory']),
player_data['x'], player_data['y'])

# 3. Clear dirty set only after successful transaction


self.dirty_players.clear()

This architecture ensures that even if the SQL server goes offline for 10 seconds, the game continues
running from Redis.

Chapter XIII: The "Item Dupe" (Transactional Integrity)


The Anatomy of an Exploit
The "Dupe" is the most economy-destroying bug in MMO history. It usually happens during Trading or
Zone Crossing.

●​ Scenario: Player A trades a Sword to Player B.


●​ The Bug: The server saves Player B (getting the sword) but crashes before saving Player A
(removing the sword).
●​ Result: Both players have the sword. The economy collapses.

To prevent this, we cannot rely on memory. We must use Atomic Transactions or Two-Phase Commits.

The "Escrow" Pattern


Never modify two inventories directly. Move the item to a temporary "Limbo" state first.

1.​ Lock: Lock both Player A and Player B's inventories.


2.​ Verify: Does Player A actually have the sword?
3.​ Deduct: Remove Sword from Player A. Add to "Trade_Escrow_Table".
4.​ Add: Remove from "Trade_Escrow_Table". Add to Player B.
5.​ Commit: Save both states simultaneously.
6.​ Unlock.

If any step fails (e.g., server crash at step 4), the database rolls back to the start. The sword returns to
Player A.

Python Logic: Atomic Trade


Python
async def process_trade(self, sender_id, receiver_id, item_uuid):
async with self.db_pool.acquire() as conn:
async with [Link](): # START TRANSACTION

# 1. Check Ownership (FOR UPDATE locks the row, preventing race conditions)
sender_inv = await [Link](
"SELECT inventory_json FROM characters WHERE id=$1 FOR UPDATE", sender_id
)
sender_items = [Link](sender_inv)

# Find the item


item = next((i for i in sender_items if i['uuid'] == item_uuid), None)
if not item:
raise Exception("Item not found or already traded!")

# 2. Remove from Sender


sender_items.remove(item)
await [Link](
"UPDATE characters SET inventory_json=$1 WHERE id=$2",
[Link](sender_items), sender_id
)

# 3. Add to Receiver
receiver_inv = await [Link](
"SELECT inventory_json FROM characters WHERE id=$1 FOR UPDATE", receiver_id
)
receiver_items = [Link](receiver_inv)
receiver_items.append(item)

await [Link](
"UPDATE characters SET inventory_json=$1 WHERE id=$2",
[Link](receiver_items), receiver_id
)

# COMMIT happens automatically here.


# If an error occurred above, it auto-ROLLBACKS.

Chapter XIV: Security (Trust No One)


The Golden Rule: The Client is a Liar
Never assume the data coming from the Unity client is true. Hackers can modify the client memory (DLL
Injection) to send whatever packets they want.

●​ Hack: Client sends DAMAGE_PACKET { target: Boss, amount: 999999 }.


●​ Defense: The server should not accept "Amount." The server calculates damage.
○​ Correct Packet: ATTACK_REQUEST { target: Boss }.
○​ Server Logic: "Player is Level 10. Sword is Level 5. Boss has 50 Armor. Damage = 20. Apply
20."

Movement Validation (Sanity Checks)


Speed hacks are common. A player modifies their client to tell the server "I moved 50 meters in 1 second"
(when max speed is 5m/s).

You cannot block every invalid move because of latency (lag might make a legitimate move look fast).
Instead, use a Leaky Bucket accumulator.

Algorithm:

1.​ Calculate expected distance: MaxSpeed * DeltaTime.


2.​ Calculate actual distance: Distance(OldPos, NewPos).
3.​ Error = Actual - Expected.
4.​ If Error > 0, add to SuspicionMeter.
5.​ If Error < 0 (player moved slower), reduce SuspicionMeter (forgive them).
6.​ If SuspicionMeter > Threshold (e.g., 50 meters of cumulative cheating), Rubberband the player
back or Ban them.

Python
def validate_movement(self, player, new_pos, dt):
max_dist = [Link] * dt
actual_dist = distance([Link], new_pos)

# Allow 10% tolerance for lag/floating point errors


if actual_dist > max_dist * 1.1:
player.suspicion_level += (actual_dist - max_dist)
else:
player.suspicion_level = max(0, player.suspicion_level - 0.5)

if player.suspicion_level > 50.0:


print(f"CHEAT DETECTED: Player {[Link]} is speed hacking.")
return False # Reject movement

return True # Accept movement

Chapter XV: Deployment and Horizontal Scaling


The Limits of a Single Server
A single Python/C# process can handle roughly 2,000–3,000 concurrent connections (CCU) before the
CPU bottleneck on the main loop becomes unmanageable. If Tantra Genesis M becomes a hit and gets
50,000 players, you need Sharding.

Sharding Strategies
There are two main ways to split the world:

1.​ Zone-Based Sharding (Seamless):


○​ Server A hosts "Mandara Village."
○​ Server B hosts "Shambala Dungeon."
○​ When a player walks from the Village to the Dungeon, their connection is "handed off" from
Server A to Server B.
○​ Challenge: What happens at the border? (Seeing players across the server line). This
requires complex server-to-server RPC communication.
2.​ Channel-Based Sharding (Classic MMO):
○​ "Mandara Channel 1", "Mandara Channel 2", etc.
○​ These are parallel universes. Players in Channel 1 cannot see players in Channel 2.
○​ This is the easiest to implement and scale. If load is high, simply spin up more Docker
containers (Channels).

Containerization (Docker)
We package the Server, the Redis instance, and the API into containers.

Dockerfile:

Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY . .
RUN pip install -r [Link]
# Expose the UDP port for Game Traffic and TCP for Admin/Chat
EXPOSE 9999/udp
EXPOSE 8080/tcp
CMD ["python", "server_main.py"]

Orchestration (Kubernetes/Agones)
For a production MMO, standard Kubernetes isn't enough because game servers are stateful. If a pod dies,
the players disconnect.

We use Agones, an open-source platform built by Google and Ubisoft for hosting game servers on
Kubernetes.1
Agones provides:

●​ GameServer Lifecycle: Ensures a server isn't shut down while players are still playing.
●​ Ping-based Routing: Connects players to the nearest physical data center (Asia, US, EU) to
minimize latency.

Conclusion: The Architecture of a Virtual World


Building Tantra Genesis M is not just about game design; it is a feat of distributed systems engineering. We
have covered the full stack:

1.​ The Transport: Moving from TCP to UDP to reduce overhead and eliminate Head-of-Line blocking.
2.​ The Logic: Implementing a deterministic Game Loop with a fixed timestep to ensure physics
consistency.
3.​ The Scale: Using Spatial Hashing (Grid) to solve the $N^2$ interaction problem.
4.​ The Illusion: Utilizing Client Prediction and Server Reconciliation to hide the unavoidable
latency of the internet.
5.​ The Persistence: Employing a Write-Back Cache (Redis) to protect the SQL database from the
firehose of real-time state changes.
6.​ The Integrity: Using Transactional Escrow logic to prevent item duplication exploits.

This architecture provides a robust foundation. It is performant enough to handle the frantic combat of an
Action RPG, secure enough to protect the in-game economy, and scalable enough to grow from a small
community to a massive population.

The code is the law of this new world. As the developer, you are not just writing software; you are defining
the physics, the economy, and the reality for thousands of people.

You might also like