Ghi Chú Phát Triển (2) - Thiết Kế Cấu Trúc Cơ Sở Dữ Liệu Redis
Tiếp nối phần trước, dựa trên yêu cầu dự án giai đoạn một, hôm qua tôi đã thiết kế định dạng dữ liệu trong cơ sở dữ liệu Redis. Redis được sử dụng như một thiết bị lưu trữ cấu trúc dữ liệu từ xa, cung cấp khả năng lưu trữ khóa-giá trị cơ bản mà không có hệ thống bảng phân tầng. Để tạo cấu trúc hai cấp, có thể lưu trữ các bảng băm (hashes) bên trong trường giá trị.
Đây là lần đầu tiên tôi áp dụng Redis trong thực tế, tuy nhiên tôi đã từng tự xây dựng các hệ thống tương tự vài năm trước với nguyên lý tương đồng. Sự ra đời của Redis như một dự án mã nguồn mở giúp loại bỏ nhu cầu “phát minh lại bánh xe”. Thiết kế hiện tại vẫn tuân theo mô hình quen thuộc từ kinh nghiệm cá nhân tôi.
Giai đoạn một dự án có yêu cầu đơn giản, không chia dữ liệu giữa các server nhưng thiết kế sẵn sàng cho việc mở rộng về sau. Về nguyên tắc, dữ liệu có thể phân chia theo tiền tố khóa (key prefix). Ví dụ: thông tin tài khoản (account) có khả năng tách riêng cao vì không liên quan đến game cụ thể.
Hệ thống người dùng sử dụng email làm tên đăng nhập, nhưng định danh duy nhất (uid) mới là khóa chính trong cơ sở dữ liệu. Người dùng được phép thay đổi tên đăng nhập (có thể đổi email). Việc nhận diện người dùng dựa trên id, do đó cơ sở dữ liệu cần chứa các khóa sau:
- account:count - counter tạo id duy nhất
- account:userlist - tập hợp id người dùng
- account:email:[email_encoded] - ánh xạ email sang id
Với account:userlist, giá trị là tập hợp (set) chứa toàn bộ id người dùng, phục vụ việc duyệt toàn bộ tài khoản. Tạm thời chức năng này chưa cần thiết và có thể gây hiệu suất kém khi số lượng người dùng lớn, nhưng thiết kế vẫn giữ để phát triển sau này.
Để xử lý ký tự đặc biệt trong email (như “:”), tôi áp dụng quy tắc mã hóa các ký tự ngoài [a-zA-Z0-9_@.] thành dạng %XX. Hàm mã hóa được viết bằng Lua:
|
|
Hàm giải mã tương ứng:
|
|
Cấu trúc dữ liệu chi tiết cho mỗi tài khoản:
- account:[id]:version - quản lý phiên bản cấu trúc
- account:[id]:email - thông tin email đã mã hóa
- account:[id]:password - lưu dạng md5(password..salt)
- account:[id]:nickname - biệt danh người dùng
- account:[id]:lastlogin - bảng hashes chứa:
- ip - địa chỉ IP lần đăng nhập cuối
- time - thời gian đăng nhập
- account:[id]:history - danh sách lịch sử đăng nhập
- account:[id]:available - trạng thái (mở/khóa/xóa)
Về bảo mật mật khẩu, chúng tôi không lưu dưới dạng văn bản rõ để tránh rò rỉ thông tin. Việc thêm “salt” khi băm md5 giúp chống lại các tấn công tra cứu bảng md5 sẵn có. Các thông tin đăng nhập cuối cùng được lưu dưới dạng bảng hashes để dễ mở rộng sau này. Khi xóa tài khoản, chỉ cập nhật trạng thái trong trường available mà không xóa vật lý dữ liệu.
Dịch vụ xác thực trung tâm
Để bảo vệ dữ liệu, tôi xây dựng dịch vụ xác thực độc lập cung cấp 3 chức năng chính:
- Đăng ký người dùng mới
- Xác thực tên đăng nhập - mật khẩu (cho dịch vụ web qua SSL)
- Xác thực thách thức (challenge) (cho ứng dụng client)
Cấu trúc dữ liệu nhân vật & thế giới ảo
- account:[id]:avatars - tập hợp id nhân vật
- avatar:count - tạo id nhân vật duy nhất
- avatar:[id]:version - phiên bản dữ liệu
- avatar:[id]:account - liên kết với tài khoản
- avatar:[id]:scene - tên cảnh hiện tại
- avatar:[id]:available - trạng thái nhân vật
- avatar:[id]:data - bảng hashes chứa:
- name - tên nhân vật
- figure - mô tả ngoại hình
- world:scene - chỉ mục cảnh trong thế giới
- scene:count - tạo id cảnh duy nhất
- scene:[id]:name - tên cảnh
- scene:[id]:available - trạng thái cảnh
- scene:[id]:info - thông tin cảnh:
- time - thời gian hoạt động
- pc - số lượng nhân vật
- scene:[id]:pc - bảng trạng thái nhân vật:
- [id] - trạng thái (online/offline)
- scene:[id]:pc:[id] - thông tin chi tiết nhân vật:
- status - trạng thái hoạt động
Mỗi tài khoản có thể sở hữu nhiều nhân vật lưu trong account:[id]:avatars. Hệ thống id nhân vật tách biệt với id tài khoản nhưng được thiết kế để dễ phân biệt qua giá trị khởi điểm khác nhau. Thông tin cảnh (scene) được lưu dưới dạng chuỗi tên cảnh và bảng hashes chi tiết.
Vấn đề kỹ thuật với Redis trên Windows
Trong thời gian chờ thiết lập máy chủ Linux, tôi gặp nhiều khó khăn khi phát triển trên Windows với Redis bản port phi chính thức. Việc kết nối socket gặp lỗi do thiếu gọi WSAStartup trong triển khai hiredis phiên bản Windows. Sau khi sửa lỗi cấu trúc bộ nhớ trong giao diện ffi của LuaJIT, vấn đề mới được giải quyết. Sự phụ thuộc vào bố cục struct C trong API hiredis khiến việc bảo trì trở nên phức tạp, tuy nhiên vẫn dễ tiếp cận hơn các thư viện C++ truyền thống.