پس از موفقیت چشمگیر در پروژهی Dockerious، تیم فنی پارک علم و فناوری پردیس متوجه نیاز به یک سیستم کش سریع و سبک برای سرویسهای داخلی شده است. با الهام گرفتن از Redis، پروژهی ساخت یک دیتابیس in-memory بومی با نام Redious به شما سپرده شده است. این دیتابیس باید بتواند دادهها را به صورت Key-Value در حافظه نگه دارد و از دیتاتایپ معمول پشتیبانی کند و یک سیاست حذف از کش LRU (Least Recently Used) برای مدیریت حافظه داشته باشد.
شما در این سوال باید استارت پیادهسازی این دیتابیس را بزنید.
جزئیات پروژه
پروژه اولیه را از این لینک دانلود کنید. ساختار پروژه به شکل زیر است:
redious/
├── datastore/
│ └── redious.go # TODO Implement
└── main.go # Just to see the sample result and run on your local
در این پروژه، شما باید اینترفیس DataStore و توابع مربوطه را در فایل datastore/redious.go پیادهسازی کنید.
آنچه باید پیادهسازی کنید
شما باید struct های cacheEntry و rediousStore را تعریف کرده و تابع سازنده NewDataStore و تمام متدهای اینترفیس DataStore را در فایل datastore/redious.go پیادهسازی کنید.
package datastore
// ZMember is used to pass score-member pairs to the ZAdd command.
type ZMember struct {
Score float64
Member string
}
// DataStore is the main interface for our in-memory database.
type DataStore interface {
Set(key, value string)
Get(key string) (string, bool)
LPush(key string, values ...string) int
RPush(key string, values ...string) int
LRange(key string, start, stop int) []string
ZAdd(key string, members ...ZMember) int
ZRange(key string, start, stop int) []string
Incr(key string) (int, error) // Atomic operations
}
// cacheEntry holds the key and the value (actual data)
type cacheEntry struct {
// TODO: Define necessary fields based on the table below.
}
// rediousStore is the internal implementation of our DataStore.
type rediousStore struct {
// TODO: Define necessary fields based on the table below.
}
// NewDataStore creates a new DataStore with a given capacity.
// The capacity determines the maximum number of keys the store can hold.
// If capacity is 0, the cache has unlimited size.
func NewDataStore(capacity int) DataStore {
// TODO: Initialize and return an instance of rediousStore
return nil // Placeholder
}
// --- String Commands ---
func (ds *rediousStore) Set(key, value string) {
// TODO: Implement
}
func (ds *rediousStore) Get(key string) (string, bool) {
// TODO: Implement
return "", false // Placeholder
}
func (ds *rediousStore) Incr(key string) (int, error) {
// TODO: Implement
return 0, nil // Placeholder
}
// --- List Commands ---
func (ds *rediousStore) LPush(key string, values ...string) int {
// TODO: Implement
return 0 // Placeholder
}
func (ds *rediousStore) RPush(key string, values ...string) int {
// TODO: Implement
return 0 // Placeholder
}
func (ds *rediousStore) LRange(key string, start, stop int) []string {
// TODO: Implement
return nil // Placeholder
}
// --- Sorted Set Commands ---
func (ds *rediousStore) ZAdd(key string, members ...ZMember) int {
// TODO: Implement
return 0 // Placeholder
}
func (ds *rediousStore) ZRange(key string, start, stop int) []string {
// TODO: Implement
return nil // Placeholder
}
**نکته مهم:** دیتابیس شما باید Thread-Safe باشد. یعنی چندین گوروتین باید بتوانند به صورت همزمان با آن کار کنند بدون اینکه race condition رخ دهد.
ساختار cacheEntry
این استراکت برای نگهداری اطلاعات مربوط به هر آیتم در کش استفاده میشود و شامل کلید و مقدار آن آیتم است.
مقدار میتواند از هر نوع دادهای باشد (رشته، لیست، مجموعه و ...).
| فیلد | کاربرد |
|---|---|
key |
نگهداری کلید هر آیتم در کش |
value |
نگهداری مقدار آیتم (قابل پشتیبانی از انواع مختلف دادهها) |
ساختار rediousStore
این استراکت پیادهسازی اصلی اینترفیس DataStore است و وضعیت کلی دیتابیس در حافظه را مدیریت میکند.
| فیلد | کاربرد |
|---|---|
capacity |
تعیین حداکثر تعداد آیتمهای قابل نگهداری در کش |
items |
نگهداری کلید و عنصر لیست برای دسترسی سریع |
ll |
نگهداری ترتیب استفاده آیتمها برای پیادهسازی الگوریتم LRU |
نیازمندیها
-
ظرفیت و LRU (Least Recently Used):
- تابع سازنده
NewDataStore(capacity int)یک ظرفیت دریافت میکند که حداکثر تعداد کلیدهای قابل ذخیره را مشخص میکند. - اگر
capacityبرابر با0باشد، دیتابیس ظرفیت نامحدود دارد و هیچ کلیدی حذف نمیشود. - اگر
capacityبزرگتر از0باشد:- هر عملیات خواندن (
Get,LRange,ZRange) یا نوشتن (Set,LPush,RPush,ZAdd,Incr) روی یک کلید، آن را به عنوان بیشترین استفادهی اخیر (MRU) علامتگذاری میکند. - زمانی که یک کلید جدید اضافه میشود و تعداد کلیدها به ظرفیت رسیده است، کلید کمترین استفادهی اخیر (LRU) باید حذف شود.
- بهروزرسانی مقدار یک کلید موجود نباید منجر به حذف کلید دیگری شود، اما همان کلید باید بهعنوان MRU بهروزرسانی شود.
- هر عملیات خواندن (
- تابع سازنده
-
مدیریت انواع داده (Type Safety):
- هر کلید تنها میتواند یکی از سه نوع داده را در خود نگه دارد: String, List, یا Sorted Set (ZSet).
- دستور
Setمیتواند نوع دادهی یک کلید را تغییر دهد (مثلاً از List به String). - سایر دستورات (مثل
LPush,RPush,ZAdd,Incr) نباید نوع موجود کلید را تغییر دهند:- اگر نوع کلید با دستور ناسازگار باشد، عملیات باید بدون تغییر داده و بدون ایجاد کلید جدید خاتمه یابد.
- مقدار بازگشتی در این حالت باید نشاندهندهی شکست باشد:
0برایLPush,RPush,ZAddfalseیا مقدار تهی ("",nil,[]) برایGet,LRange,ZRange- خطا (
error) برایIncr
دستورات String
String**نیازمندیها:**
-
Set(key, value string)- مقدار رشتهای
valueرا برای کلیدkeyتنظیم میکند. - اگر کلید از قبل وجود داشته باشد (از هر نوعی)، مقدار و نوع آن کاملاً جایگزین میشود.
- مقدار رشتهای
-
Get(key string) (string, bool)- مقدار رشتهای ذخیرهشده برای
keyرا برمیگرداند. - اگر کلید وجود نداشته باشد یا از نوع رشتهای نباشد، باید
""وfalseبرگرداند.
- مقدار رشتهای ذخیرهشده برای
-
Incr(key string) (int, error)- مقدار رشتهای ذخیرهشده در
keyرا به عدد صحیح تبدیل کرده و یک واحد به آن اضافه میکند. - اگر کلید وجود نداشته باشد، باید با مقدار
"1"ایجاد شود و مقدار1وnilبرگرداند. - اگر کلید وجود داشته باشد ولی از نوع رشتهای نباشد، باید خطا با پیام
value at key is not a stringبرگرداند. - اگر کلید وجود داشته باشد ولی مقدار رشتهای آن قابل تبدیل به عدد صحیح نباشد (مثلاً
"abc"یا"")، باید خطا با پیامvalue at key is not an integerبرگرداند. - در صورت موفقیت، باید مقدار جدید (بهروزشده) و
nilبرگردانده شود. - این عملیات باید اتمیک (atomic) باشد.
- مقدار رشتهای ذخیرهشده در
دستورات List
List**نیازمندیها:**
-
LPush(key string, values ...string) int- یک یا چند مقدار را به ابتدای لیست اضافه میکند.
- ترتیب اضافه شدن باید مطابق رفتار Redis باشد: آخرین آرگومان ورودی (
values)، اولین عضو جدید لیست میشود. (برای مثالLPush key a b cلیست را به[c, b, a, <old_items>]تبدیل میکند). - اگر کلید وجود نداشته باشد، لیست جدید ایجاد میشود.
- اگر نوع کلید متفاوت باشد، عملیات انجام نمیشود و
0برمیگردد. - در صورت موفقیت، طول جدید لیست بازگردانده میشود.
-
RPush(key string, values ...string) int- مشابه
LPushاست ولی مقادیر را به انتهای لیست اضافه میکند. - سایر رفتارها مشابه
LPushاست.
- مشابه
-
LRange(key string, start, stop int) []string- اعضای لیست را در بازهی
[start, stop]بازمیگرداند. - از ایندکسهای منفی پشتیبانی میکند.
- اگر کلید وجود نداشته باشد یا نوع آن
Listنباشد، باید[]string{}برگرداند. - نکته مهم: اسلایس بازگشتی باید یک کپی از دادههای داخلی باشد، نه یک ارجاع مستقیم به آن.
- اعضای لیست را در بازهی
دستورات Sorted Set (ZSET)
Sorted Set (ZSET)**نیازمندیها:**
-
ZAdd(key string, members ...ZMember) int- یک یا چند عضو دارای امتیاز (
Score) را به مجموعهی مرتبشده اضافه میکند. - اگر کلید وجود نداشته باشد، مجموعهی جدید ایجاد میشود.
- اگر کلید نوع دیگری داشته باشد، عملیات انجام نمیشود و
0برمیگردد. - اگر عضوی از قبل وجود داشته باشد، امتیاز آن باید بهروزرسانی شود.
- مجموعه باید همواره بر اساس امتیاز (و در صورت تساوی، نام عضو به ترتیب الفبایی) مرتب بماند.
- مقدار بازگشتی باید تعداد اعضای جدیدی باشد که به مجموعه اضافه شدهاند.
- اگر یک
Memberچند بار در همان فراخوانیZAddتکرار شود، تنها آخرین Score آن معتبر است.
- یک یا چند عضو دارای امتیاز (
-
ZRange(key string, start, stop int) []string- اعضای مجموعهی مرتبشده را بر اساس رتبه در بازهی
[start, stop]بازمیگرداند. - از ایندکسهای منفی پشتیبانی میکند.
- اگر کلید وجود نداشته باشد یا نوع آن Sorted Set نباشد، باید
[]string{}برگرداند.
- اعضای مجموعهی مرتبشده را بر اساس رتبه در بازهی
نکات
- تمرکز اصلی این سؤال بر روی مدیریت صحیح ساختار دادههای داخلی، همزمانی (Concurrency) و منطق LRU است.
چه چیزی را آپلود کنید
پس از پیادهسازی کامل، صرفا تک فایل datastore/redious.go را آپلود کنید.
ارسال پاسخ برای این سؤال