C# Geeks (.NET)
549 subscribers
157 photos
4 videos
177 links
Download Telegram
سلام عزیزان
کمی بخاطر سر شلوغی این موضوع عقب افتاد اما می تونید جزئیات میت ها رو در سایت زیر ببینید.

https://thisisnabi.dev

برای پرداخت 10x developer همون طور که قول دادم 50% تخفیف بگیرید.
اما چون system design هنوز پیش ثبت نامه شرایط پرداخت 4 قسطه اسنپ پی رو اگر دارین می تونید باهاش پرداخت انجام بدید.
برای جزئیات پرداخت به @thisisnabi_admin پیام بدید.
📌 200+ سوال مصاحبه واقعی (بخش 5️⃣)

[ BUCKET 3 — CACHING, SCHEDULING, MESSAGING, AND SECURITY ]

⚡️ فصل 44 — Caching

💾 165. ءIDistributedCache چیست و چه تفاوتی با Memory Cache دارد؟
⏳ 166. چگونه Cache Expiration و Eviction Policyها را برای Redis پیاده‌سازی می‌کنید؟
🚀 167.ءOutputCache در ASP.NET Core چیست و چگونه آن را برای Endpointها پیکربندی می‌کنید؟
🎯 168. چگونه می‌توان OutputCache را بر اساس پارامترهای Request یا هویت کاربر (User Identity) تغییر داد؟
🔀 169. ءHybridCache چیست و چگونه In-Memory Cache و Distributed Cache را با یکدیگر ترکیب می‌کند؟
🏗 170. چگونه از HybridCache با یک استراتژی L1/L2 در ASP.NET Core استفاده می‌کنید؟
🔥 171. ءFusionCache چیست و چه مشکلی را که HybridCache دارد، حل می‌کند؟

⏰ فصل 49 — Task Scheduling

⚙️ 172. ءBackground Service در ASP.NET Core چیست؟ چه زمانی Start و چه زمانی Stop می‌شود؟
🔄 173. تفاوت بین Background Service و IHostedService چیست؟
🚨 174. اگر یک Unhandled Exception در یک Background Service رخ دهد، چه اتفاقی می‌افتد؟
📅 175. چگونه با استفاده از Quartz.NET و Cron Expressionها، Taskهای تکرارشونده (Recurring Tasks) را زمان‌بندی می‌کنید؟
💉 176. چگونه Dependencyها را داخل Quartz Jobها Inject می‌کنید؟
🛑 177. برای جلوگیری از اجرای چندباره یک Scheduled Job در چند Instance مختلف، از چه استراتژی‌هایی می‌توانید استفاده کنید؟
🌐 178. چگونه یک راهکار Distributed Scheduling را برای اجرای Taskها روی چند Server معماری می‌کنید؟

📨 فصل 50 — Event Messaging

🔄 179. ءMediatR چیست و کدام Design Pattern را پیاده‌سازی می‌کند؟
📢 180. چگونه با استفاده از MediatR، Notification ارسال و Handle می‌کنید؟
⚠️ 181. استفاده بیش از حد (Overuse) از MediatR در یک پروژه چه معایب و مشکلاتی می‌تواند ایجاد کند؟
📨 182. ءMassTransit چیست و چه نقشی در Event-Driven Architecture دارد؟
🐇 183. چگونه MassTransit را با RabbitMQ یا یک Message Broker دیگر پیکربندی می‌کنید؟
📦 184. چگونه Messageها (Event / Command) را با استفاده از MassTransit تعریف و Consume می‌کنید؟
🚀 185. قابلیت‌های پیشرفته MassTransit مانند Sagaها یا Message Retry Policyها چیستند؟

[ BUCKET 4 — SYSTEM DESIGN AND ARCHITECTURE ]

🌐 فصل 51 — APIs، SDKs و Resilience

⚠️ 186. اگر برای هر API Call یک HttpClient جدید ایجاد کنید، چه اتفاقی می‌افتد؟
🏭 187. ءHttpClientFactory چیست و چرا در ASP.NET Core معرفی شد؟
🎯 188. ءTyped Client چیست و چه تفاوتی با Named Client دارد؟
🔗 189. ءRefit چیست و چگونه با استفاده از آن یک API Interface تعریف می‌کنید؟
⛓️ 190. ءDelegatingHandler چیست و کاربردهای رایج آن چیستند؟
🛡 191. ءPolly چیست و چگونه در کنار HttpClientFactory استفاده می‌شود؟
🔄 192. یک مثال از اضافه کردن Retry Policy با استفاده از Polly به HttpClientFactory ارائه دهید.
🚨 193. چگونه استراتژی Circuit Breaker را با استفاده از Polly پیاده‌سازی می‌کنید؟
🛟 194. چگونه استراتژی Fallback را با استفاده از Polly پیاده‌سازی می‌کنید؟
🔐 195. چگونه Authentication و Token Refresh را برای Outgoing HTTP Requests مدیریت می‌کنید؟

🔭 فصل 52 — OpenTelemetry & Observability

📊 196. سه ستون اصلی (Three Pillars) مربوط به Observability در OpenTelemetry چیستند؟
🏗 197. ءOpenTelemetry Collector چیست و چه نقشی دارد؟
🔍 198. در مفهوم Distributed Tracing، یک Span و یک Trace چیستند؟
🔗 199. چگونه Distributed Context Propagation و Baggage را بین Microserviceها مدیریت می‌کنید؟
🎯 200. ءSampling در OpenTelemetry چیست و چرا از Strategyهای مختلف Sampling استفاده می‌کنید؟
📈 201. چگونه در دات نت، Custom Metrics مانند Counterها و Histogramها ایجاد می‌کنید؟
🔗 202. ءTrace Links در OpenTelemetry چیستند و چه زمانی مفید هستند؟
📊 203.چگونه OpenTelemetry را برای مدیریت داده‌های High-Cardinality در Metricها پیکربندی می‌کنید؟

🐳 فصل 54 — Build & Deploy / Distributed Systems

📦 204.تفاوت بین Framework-Dependent Deployment و Self-Contained Deployment چیست؟
🎯 205.چگونه یک برنامه NET. را برای یک Runtime مشخص، مانند win-x64 یا linux-x64، Publish می‌کنید؟
⚙️ 206.چگونه فایل‌های appsettings مخصوص هر Environment را در Publish Output پیکربندی می‌کنید؟
🐳 207.چگونه برای یک برنامه ASP.NET Core یک Dockerfile می‌نویسید؟
🏗 208.ءMulti-Stage Docker Build چیست و چرا باید از آن استفاده کنید؟
📦 209.چگونه حجم (Size) یک Docker Image را برای برنامه‌های NET. بهینه می‌کنید؟
🔐 210.چگونه Environment Variableها و Configuration را به یک Docker Container منتقل می‌کنید؟
📌 برای Order فقط یک Status داشته باشیم یا بریم سراغ State Machine؟ 🤔
فرض کن داریم یک فروشگاه اینترنتی میسازیم و Orderمون چندتا وضعیت داره:
Pending
Paid
Processing
Shipped
Delivered
Cancelled

خب معلومه دیگه 😎
یک enum می‌سازیم:
public enum OrderStatus
{
Pending,
Paid,
Processing,
Shipped,
Delivered,
Cancelled
}

بعد داخل Order:
public OrderStatus Status { get; private set; }

هرجا هم خواستیم وضعیت رو عوض کنیم:
order.Status = OrderStatus.Paid;

تموم شد رفت. 😎
هم ساده‌ست، هم خواناست، هم Database هم فقط یک ستون Status داره.
ولی یه لحظه صبر کن...
واقعاً هر Statusای می‌تونه به هر Status دیگه‌ای تبدیل بشه؟ 🤨
مثلاً:
Pending → Paid
Paid → Processing
Processing → Shipped
Shipped → Delivered

این‌ها منطقی به نظر میرسن.
ولی این چی؟
Delivered → Pending

یا:
Shipped → Paid

یا حتی:
Cancelled → Shipped

احتمالاً نه!
پس مشکل از خود Status نیست.
مشکل اینجاست که اگر فقط یک enum داشته باشیم، این enum به‌تنهایی هیچ چیزی درباره‌ی قوانین انتقال بین وضعیت‌ها نمیگه.
یعنی این کد:
order.Status = OrderStatus.Delivered;

از نظر #C کاملاً معتبره.
ولی از نظر Business ممکنه کاملاً غیرمعتبر باشه.
اینجاست که معمولاً یکی میگه:
«پس قبلش if می‌ذاریم.»

مثلاً:
if (order.Status != OrderStatus.Shipped)
throw new InvalidOperationException();

order.Status = OrderStatus.Delivered;

خب...
بعد یک ماه میشه:
if (status == OrderStatus.Pending)
{
...
}
else if (status == OrderStatus.Paid)
{
...
}
else if (status == OrderStatus.Processing)
{
...
}

بعد یک Requirement جدید میاد:
اگر Payment Failed شد، Order دوباره Payment بشه.

بعد یکی دیگه:
اگر Customer درخواست Cancellation داد، فقط قبل از Shipment اجازه بده.

بعد:
ءAdmin بتونه یک Order رو از حالت Processing به Cancelled ببره، ولی Customer نتونه.
و ناگهان...💀 Business Ruleهای مربوط به Lifecycle سفارش پخش شدن توی:
Controller
Service
Handler
Domain
Background Job
...

و هرکس هم یک قانون متفاوت نوشته.
اینجا دقیقاً جاییه که State Machine می‌تونه ارزش پیدا کنه.
ءState Machine اساساً میگه:
«ءOrder فقط یک Status نداره؛ یک Lifecycle داره و فقط بعضی Transitionها مجاز هستند.»
حالا به‌جای اینکه هرجای سیستم بنویسیم:
order.Status = OrderStatus.Shipped;

میگیم:
order.Ship();

و خود Domain تصمیم می‌گیره آیا این Transition مجازه یا نه.
مثلاً:
public void Ship()
{
if (Status != OrderStatus.Processing)
throw new InvalidOperationException(
"Only processing orders can be shipped.");

Status = OrderStatus.Shipped;
}

حالا اگر کسی بگه:
order.Ship();

و Order هنوز Pending باشه، خود Domain جلوش رو می‌گیره.
این خیلی بهتر از اینه که امیدوار باشیم همه‌ی Callerها قبلش if درست نوشته باشن. 😏
اماااا...
اینجا هم نباید سریع نتیجه بگیریم:
«پس State Machine همیشه بهتره!»

نه.
اگر سیستم ما یک Order خیلی ساده داره:
Pending → Completed

واقعاً لازم نیست برای دو وضعیت یک State Machine عظیم درست کنیم.
پس State Machine قرار نیست صرفاً چون اسمش خفن‌تره وارد پروژه بشه.
مسئله اینه که:
آیا Lifecycle موجودیت، خودش دارای پیچیدگی Business است؟
اینجا State Machine می‌تونه خیلی خواناتر و قابل‌کنترل‌تر باشه.
حتی می‌تونی Ruleهایی مثل این داشته باشی:
Customer فقط تا قبل از

Shipped
می‌تونه Cancellation درخواست کنه.

یا:

ءRefund فقط وقتی مجازه که Payment موفق بوده باشه.

یا:
ءShipment فقط بعد از Payment موفق ایجاد بشه.

این‌ها دیگه صرفاً Status نیستن.
این‌ها Business Rules مربوط به Transitionها هستن.
بعضیا فکر می‌کنن وقتی State Machine داریم، دیگه Status لازم نیست.
نه! State Machine و Status لزوماً رقیب هم نیستن.
🗄 10 قابلیت کمترشناخته‌شده SQL که هر Developer باید بداند(پارت 1️⃣)
بیشتر Developerها احتمالاً فقط از حدود 20 درصد قابلیت‌های SQL استفاده می‌کنند.
آن‌ها SELECT، JOIN و GROUP BY می‌نویسند و همان‌جا متوقف می‌شوند.
اما SQL یک لایه‌ی دوم هم دارد؛ قابلیت‌هایی که می‌توانند یک صفحه کد Application یا سه Query جداگانه را به یک Statement تمیز و یکپارچه تبدیل کنند.⚡

ءDeveloperهای Senior همیشه به سراغ این قابلیت‌ها می‌روند. بسیاری از Developerهای Junior و Mid-level حتی یک‌بار هم آن‌ها را ندیده‌اند.
هیچ‌کدام از این قابلیت‌ها جدید یا عجیب‌وغریب نیستند. آن‌ها همین حالا در Databaseای که استفاده می‌کنید وجود دارند و منتظرند تا از آن‌ها استفاده کنید.
🚀امروز می‌خواهم ۱۰ قابلیت کمترشناخته‌شده‌ی SQL را به شما نشان بدهم که هر Developer باید آن‌ها را بشناسد.

📌در این مطلب، موارد زیر را بررسی می‌کنیم:

🔸️Common Table Expressions یا CTE
🔹️Window Functions
🔸️LATERAL Joins
🔹️GROUPING SETS، ROLLUP و CUBE
🔸️عبارت FILTER در Aggregateها
🔹️UPSERT با استفاده از INSERT ... ON 🔸️CONFLICT
🔹️پشتیبانی از JSON
🔸️Computed / Generated Columns
🔹️TABLESAMPLE
🔸️Partial Indexes

بریم سراغشون. 🚀

تمام Queryهای این مطلب روی Database
ءPostgreSQL تست شده‌اند. بیشتر این قابلیت‌ها در Databaseهای دیگر نیز وجود دارند، اما Syntax دقیق آن‌ها ممکن است متفاوت باشد؛ در طول مطلب به تفاوت‌های اصلی اشاره می‌کنم.

1️⃣ ءCommon Table Expressions یا CTE

یک Query پیچیده که همه‌چیز در یک Statement داخل آن فشرده شده باشد، خواندنش سخت و تغییر دادنش حتی سخت‌تر است.
ءCommon Table Expression یا همان CTE به شما اجازه می‌دهد با استفاده از Keyword مربوط به WITH، آن Query را به چند مرحله‌ی نام‌گذاری‌شده و پشت‌سرهم تقسیم کنید.🧩
هر مرحله مانند یک Result موقت و نام‌گذاری‌شده است که می‌توانید در مراحل بعدی روی آن کار کنید.
WITH recent_shipments AS (
SELECT
s.id,
s.number,
s.carrier,
s.status,
s.created_at
FROM shipments.shipments s
WHERE s.created_at >= CURRENT_DATE - INTERVAL '30 days'
),
shipment_details AS (
SELECT
rs.number,
rs.carrier,
rs.status,
COUNT(si.id) AS total_items,
SUM(si.quantity) AS total_quantity
FROM recent_shipments rs
LEFT JOIN shipments.shipment_items si
ON rs.id = si.shipment_id
GROUP BY
rs.number,
rs.carrier,
rs.status
)
SELECT
number AS shipment_number,
carrier,
status,
total_items,
total_quantity
FROM shipment_details
ORDER BY total_quantity DESC;

این Query شامل دو بخش نام‌گذاری‌شده است. recent_shipments، Shipmentهایی را که در 30 روز گذشته ایجاد شده‌اند انتخاب می‌کند.📅
سپس shipment_details روی آن Result کار می‌کند، Itemهای مربوط به Shipment را Join می‌کند و تعداد و مقدار آن‌ها را Aggregate می‌کند.
در نهایت، SELECT اصلی از shipment_details می‌خواند؛ درست مثل اینکه با یک Table معمولی کار می‌کند.
مزیت اصلی این است که Query را می‌توانید از بالا به پایین بخوانید؛ درست مثل مراحل یک دستورالعمل، به‌جای اینکه مجبور باشید Nested Subqueryها را از داخل به بیرون دنبال کنید.🧠
ءCTEها از Recursive Query نیز پشتیبانی می‌کنند؛ با استفاده از WITH RECURSIVE.
این قابلیت زمانی بسیار کاربردی است که با داده‌های Hierarchical مانند:
🏢ساختار سازمانی
🌳درخت دسته‌بندی‌ها
📂ءCategory Tree
🔗ساختار Parent/Child
کار می‌کنید.
یک Common Table Expression می‌تواند داخل Statementهای SELECT، INSERT، UPDATE یا DELETE استفاده شود.
Forwarded from Mahi in Tech
توی سیستم‌های High-Load، یکی از چالش‌های همیشگی اینه که خیلی سریع بفهمیم یک دیتای خاص وجود داره یا نه. اگر بخوایم برای هر چک کردن ساده به‌طور مستقیم سراغ دیتابیس بریم یا حتی به صورت کامل روی Cache حساب کنیم، هم منابع زیادی درگیر می‌شه و هم Latency بالا میره.

یکی از رویکردهای بهینه و جذاب برای حل این مسئله، استفاده از Bloom Filter هست.
بلوم فیلتر یک Data Structure احتمالاتی 🥴 هست که با کمترین میزان مصرف مموری و سرعت خیره‌کننده، بهمون میگه یک آیتم وجود داره یا نه.

سناریوی واقعی: انتخاب یوزرنیم در تلگرام
تلگرام صدها میلیون کاربر داره. وقتی شما موقع ثبت‌نام داری یوزرنیم تایپ می‌کنی، به ازای هر کاراکتری که می‌زنی باید چک بشه که این یوزرنیم آزاد هست یا نه. اگر تلگرام بخواد برای هر تایپ شما یک کوئری به دیتابیس اصلیش بزنه، دیتابیس در عرض چند ثانیه از حجم درخواست‌ها نابود می‌شه!
راه‌حل چیه؟ تلگرام تمام یوزرنیم‌های ثبت‌شده رو میده به یک Bloom Filter که توی رم قرار داره. وقتی شما یوزرنیم جدید رو تایپ می‌کنی، بلوم فیلتر در کسری از میلی‌ثانیه چک می‌کنه. اگر بگه «این یوزرنیم به‌طور قطع وجود نداره»، تلگرام همون لحظه تیک سبز رو بهت نشون میده و دیگه کاری به دیتابیس نداره (صرفه‌جویی عظیم در منابع). اما اگر بلوم فیلتر بگه «ممکن هست وجود داشته باشه»، تلگرام تازه اونجا میره از دیتابیس می‌پرسه که "مطمئنی این یوزرنیم پر شده؟" تا وضعیت دقیق رو بهت بگه. (که البته تلگرام چنین کاری نمی‌کنه و مثال بود=))

حالا این بلوم فیلتر چطور کار می‌کنه؟
پشت صحنه، Bloom Filter در واقع فقط یک آرایه طولانی از Bitهاست که اول کار همه‌شون صفر هستن. در کنارش، چند تا تابع Hash مستقل و سریع هم داریم.
وقتی می‌خوایم یک دیتای جدید رو به سیستم اضافه کنیم، این دیتا رو به توابع Hash می‌دیم. خروجی این توابع، ایندکس‌هایی از همون آرایه بیت‌هاست. بعد میریم اون ایندکس‌ها رو برابر با ۱ قرار می‌دیم.

موقع جستجو دوباره همون دیتای ورودی رو هش می‌کنیم و ایندکس‌ها رو چک می‌کنیم:
۱. اگر حتی یکی از اون بیت‌ها صفر باشه، سیستم با قطعیت ۱۰۰٪ میگه این دیتا «به‌طور قطع وجود نداره».
۲. اگر همه بیت‌ها ۱ باشن، سیستم میگه این دیتا «احتمالا وجود داره».

چرا می‌گیم احتمالا؟ چون ممکنه اون بیت‌ها قبلا به‌خاطر هش شدنِ دیتای دیگه‌ای ۱ شده باشن (همون پدیده Hash Collision). یعنی ما توی Bloom Filter خطای False Positive داریم، اما False Negative اصلا نداریم.

البته که این ساختار محدودیت‌های خودش رو هم داره؛ به‌طور مثال حذف کردن یک آیتم از بلوم فیلتر در پیاده‌سازی‌های استانداردش یه‌جورایی غیرممکنه؛ چون با صفر کردن یک بیت، ممکن هست دیتای دیگه‌ای که از همون بیت استفاده می‌کرده رو هم خراب کنیم.

در نهایت، در ازای پذیرش اون احتمال کمِ False Positive، سیستمی به دست میاد که می‌تونه وجود میلیون‌ها رکورد رو فقط با چند مگابایت RAM در لایه Application چک کنه و زیرساخت دیتابیس شما رو از شر درخواست‌های بیهوده نجات بده.
📦 چطور یک فایل چندگیگابایتی را Upload می‌کنیم و اگر اینترنت قطع شد، از همان‌جا ادامه می‌دهیم؟
فرض کن داری یک فایل 8GB آپلود می‌کنی.
۵۰٪ آپلود شده...
Upload: 4.0 GB / 8.0 GB

بعد اینترنت قطع می‌شود. 😐
اگر Upload به‌صورت یک درخواست ساده انجام شده باشد، ممکن است مجبور شوی دوباره از ابتدا شروع کنی.
یعنی:
8 GB
↓
Connection Lost
↓
💀
↓
Upload again

اما سیستم‌های Resumable Upload دقیقاً برای حل همین مشکل طراحی شده‌اند.
ایده‌ی اصلی خیلی ساده است:
فایل بزرگ را طوری منتقل کن که بتوانیم بدانیم تا کجای آن با موفقیت دریافت شده و بعداً از همان نقطه ادامه دهیم.


📌 به‌جای یک Upload بزرگ، انتقال را قابل ادامه می‌کنیم
در یک Upload معمولی ممکن است کل فایل در یک درخواست ارسال شود:
Client
|
|------ 8GB ------> Server

اگر ارتباط وسط انتقال قطع شود، درخواست شکست می‌خورد و ادامه‌دادن آن دشوار است.
یک مثال واقعی از پروتکل tus
فرض کنیم Upload تا byte شماره‌ی 70 پیش رفته است. Client وضعیت Upload را می‌پرسد:
HEAD /files/abc123

Server پاسخ می‌دهد:
Upload-Offset: 70

یعنی:
تا byte 70

قبلاً دریافت شده.
حالا Client ادامه‌ی فایل را می‌فرستد:
PATCH /files/abc123

Upload-Offset: 70
Content-Type: application/offset+octet-stream

[remaining bytes]

ءServer بعد از دریافت آن بخش پاسخ می‌دهد:
Upload-Offset: 100

یعنی Upload حالا تا byte شماره‌ی 100 پیش رفته است.
این رفتار در specification رسمی tus تعریف شده و صرفاً یک الگوی پیشنهادی نیست.
در Resumable Upload، انتقال در چند درخواست انجام می‌شود:
Client
|
|--- Part 1 ---> Server
|
|--- Part 2 ---> Server
|
|--- Part 3 ---> Server
|
|--- Part 4 ---> Server
|
...

در مستندات رسمی Google Cloud نیز Resumable Upload دقیقاً به‌عنوان روشی برای ادامه‌دادن انتقال بعد از اختلال ارتباط معرفی شده است؛ هر درخواست می‌تواند بخشی از Object را منتقل کند.

📌ءPause هم در اصل یعنی چه؟
یک نکته‌ی مهم:
ءPause و Resume الزاماً دو قابلیت کاملاً جدا نیستند.
وقتی Upload قابل Resume باشد، Client می‌تواند ارسال داده را متوقف کند و بعداً با همان Upload Session ادامه دهد؛ به شرط اینکه Session هنوز معتبر باشد.
مثلاً:
10:00
Upload → 2GB

10:05
Pause ⏸️

10:30
Resume ▶️

Continue → 2GB

در Google Cloud Storage، یک Resumable Upload با یک session URI انجام می‌شود و همان session برای ادامه‌ی انتقال استفاده می‌شود. مستندات Google می‌گوید این session می‌تواند تا یک هفته فعال بماند.
پس یک نکته‌ی مهم داریم:
ءResume به وجود یک state/session قابل ادامه وابسته است.

آیا همیشه باید فایل را به Chunkهای کوچک تقسیم کنیم؟
نه.
این یکی از جاهایی است که خیلی از توضیحات ساده، بیش از حد کلی‌گویی می‌کنند. Resumable Upload الزاماً به معنی این نیست که:
8GB
↓
8000 × 1MB

حتماً باید چنین کاری انجام دهیم.
ءGoogle Cloud صراحتاً اشاره می‌کند که در بعضی شرایط بهتر است انتقال در یک chunk بزرگ انجام شود و chunkهای کوچک‌تر هزینه و latency بیشتری ایجاد می‌کنند. Chunking زمانی می‌تواند مفید باشد که مثلاً محدودیت اندازه‌ی درخواست وجود داشته باشد یا بخواهیم مقدار داده‌ای که در صورت شکست دوباره باید ارسال شود را محدود کنیم.
پس:
ءChunking یک تکنیک برای پیاده‌سازی و کنترل Upload است؛ Resumability مفهوم بزرگ‌تری است.

ءIntegrity Check هم مهم است
فرض کن فایل 8GB است.
ما فقط نمی‌خواهیم بگوییم:
8GB received ✅

باید مطمئن شویم:
چیزی که دریافت کرده‌ایم همان چیزی است که Client قصد ارسالش را داشته.

برای همین، سیستم‌های Storage می‌توانند از checksum / hash برای بررسی integrity استفاده کنند.
ءGoogle Cloud در مستندات Resumable Upload توصیه می‌کند برای Object نهایی integrity check انجام شود؛ از جمله استفاده از Content-MD5 برای بررسی اینکه Object نهایی با فایل اصلی مطابقت دارد.
این موضوع مخصوصاً برای فایل‌های بزرگ اهمیت بیشتری پیدا می‌کند، چون انتقال آن‌ها زمان بیشتری طول می‌کشد.
#کلیشه
«این متن با AI نوشته شده، پس ارزش خوندن نداره.»
از کجا معلوم؟
شاید نویسنده، حرف خوبی برای گفتن داشته باشه، ولی نویسنده‌ خوبی نباشه.
شاید فقط ایده رو داده باشه به AI
تا بهتر بیانش کنه.
ناتوانی در نوشتن، لزوما ناتوانی در فکر کردن نیست.
اگه همون حرف رو یک نویسنده‌ی ضعیف ولی یک مهندس قوی گفته باشه و AI فقط بهتر بیانش کرده باشه، چی؟
حالا سوال:
ما کیفیت فکر رو قضاوت می‌کنیم
یا کیفیت جمله‌بندی رو؟
🗄 10 قابلیت کمترشناخته‌شده SQL که هر Developer باید بداند(پارت 2️⃣)


🪟 2. Window Functions

گاهی به محاسبه‌ای روی Rowهای مرتبط نیاز دارید، اما همچنان می‌خواهید هر Row به‌صورت جداگانه در نتیجه باقی بماند.
یک GROUP BY، Rowها را به یک Row برای هر Group تبدیل می‌کند. اما یک Window Function روی مجموعه‌ای از Rowها — یعنی همان Window — محاسبه انجام می‌دهد، در حالی که هر Row را دست‌نخورده حفظ می‌کند.
SELECT
number,
carrier,
created_at,
ROW_NUMBER() OVER (
PARTITION BY carrier
ORDER BY created_at DESC
) AS shipment_sequence,
RANK() OVER (
PARTITION BY carrier
ORDER BY created_at DESC
) AS shipment_rank
FROM shipments.shipments;

SELECT
number,
status,
created_at,
LAG(status) OVER (
ORDER BY created_at
) AS previous_status,
LEAD(carrier) OVER (
ORDER BY created_at
) AS next_carrier
FROM shipments.shipments;

ءQuery اول، Shipmentهای هر Carrier را بر اساس تاریخ رتبه‌بندی می‌کند. ROW_NUMBER() یک Sequence یکتا درون هر Carrier ایجاد می‌کند؛ یعنی همان PARTITION BY carrier. اما RANK() نیز همین کار را انجام می‌دهد، با این تفاوت که Rowهای دارای مقدار برابر، رتبه‌ی مشترک دریافت می‌کنند.
ءQuery دوم از LAG و LEAD استفاده می‌کند تا بدون نیاز به Self-Join، به Row قبلی و بعدی در ترتیب مشخص‌شده نگاه کند؛ در اینجا، Status قبلی و Carrier بعدی.
ءWindow Functionها روشی هستند که با استفاده از آن‌ها می‌توانید Running Total، Ranking، Moving Average و مقایسه‌ی Rowبه‌Row بسازید.
آن‌ها بخشی از Standard SQL هستند و در PostgreSQL، SQL Server، Oracle و MySQL 8+ کار می‌کنند.

🔗 3. LATERAL Joins

یک JOIN معمولی، دو Table را بر اساس یک شرط با یکدیگر Match می‌کند. اما نمی‌تواند برای هر Row از Table اول، یک Query جداگانه اجرا کند.
یک LATERAL JOIN می‌تواند این کار را انجام دهد. این قابلیت به یک Subquery در سمت راست اجازه می‌دهد به Columnهای Table سمت چپ Reference داشته باشد و برای هر Row، یک‌بار اجرا شود؛ بنابراین برای مسئله‌های Top-N-Per-Group بسیار مناسب است.
-- For each carrier, grab their single most recent shipment

SELECT
c.carrier,
s.number,
s.status,
s.created_at
FROM (
SELECT DISTINCT carrier
FROM shipments.shipments
) c
CROSS JOIN LATERAL (
SELECT
number,
status,
created_at
FROM shipments.shipments
WHERE carrier = c.carrier
ORDER BY created_at DESC
LIMIT 1
) s;

برای هر Carrier منحصربه‌فرد، Subquery مربوط به LATERAL جدیدترین Shipment آن Carrier را انتخاب می‌کند؛ یعنی ORDER BY created_at DESC LIMIT 1.
نکته‌ی اصلی این بخش عبارت WHERE carrier = c.carrier است. Query داخلی می‌تواند carrier مربوط به Row بیرونی را ببیند؛ قابلیتی که یک Subquery معمولی نمی‌تواند انجام دهد.
این روش، تمیزترین راه برای بیان مسئله‌ی «جدیدترین Row برای هر Group» یا «۳ مورد برتر برای هر Category» است، بدون اینکه به Window Function نیاز داشته باشید.
نکته: در SQL Server، همین قابلیت با CROSS APPLY نوشته می‌شود و برای نسخه‌ی مشابه Left Join از OUTER APPLY استفاده می‌شود. PostgreSQL از CROSS JOIN LATERAL و LEFT JOIN LATERAL استفاده می‌کند.
یه چیز عجیب درباره‌ی Productivity:
گاهی اضافه کردن آدم به یک پروژه،
پروژه رو سریع‌تر نمیکنه.
ممکنه حتی کندترش کنه.
چون آدم جدید یعنی:
ءContext جدید
جلسه‌ی بیشتر
ءCommunication بیشتر
ءCoordination بیشتر
و گاهی Dependency بیشتر.
برای همین:
10 نفر روی یک مسئله، لزوماً از 3 نفر سریع‌تر نیستن.
گاهی مشکل کمبود آدم نیست.
مشکل اینه که:
مسئله بیش از حد آدم میخواد تا حل بشه.
و اونجا شاید باید Architecture یا Process رو ساده‌تر کرد، نه Team رو بزرگ‌تر.
🚕 اوبر چگونه نزدیک‌ترین راننده را در مقیاس بزرگ پیدا می‌کند؟

فرض کن در یک شهر بزرگ، هزاران یا حتی میلیون‌ها راننده و مسافر به‌صورت هم‌زمان در حال حرکت هستند.
یک مسافر درخواست سفر می‌دهد و سیستم باید خیلی سریع جواب بدهد:
کدام راننده را به این مسافر اختصاص بدهیم؟

در نگاه اول، جواب ساده است:
تمام راننده‌ها را بگیر
فاصله‌ی هرکدام تا مسافر را حساب کن
نزدیک‌ترین را انتخاب کن

اما این راه‌حل در مقیاس بزرگ، خیلی زود تبدیل به یک مشکل جدی می‌شود. 😅
❌ چرا بررسی همه‌ی راننده‌ها جواب خوبی نیست؟
فرض کن در یک شهر، ۱۰۰ هزار راننده‌ی آنلاین داریم.
اگر برای هر درخواست سفر، فاصله‌ی تمام ۱۰۰ هزار راننده را بررسی کنیم، هزینه‌ی هر درخواست تقریباً به تعداد کل راننده‌ها وابسته می‌شود.
حالا اگر هزاران درخواست در هر لحظه وارد شوند، سیستم باید دائماً:
موقعیت راننده‌ها را دریافت کند؛ فاصله‌ی آن‌ها را محاسبه کند؛ راننده‌های نامناسب را حذف کند؛
و در نهایت بهترین گزینه را انتخاب کند.
مشکل فقط تعداد راننده‌ها نیست.
موقعیت راننده‌ها هم ثابت نیست. هر چند لحظه ممکن است راننده:
چند خیابان جلوتر رفته باشد؛ سفر جدیدی قبول کرده باشد؛ آفلاین شده باشد؛
یا دیگر برای دریافت سفر در دسترس نباشد.
پس ما با یک Query ساده‌ی Database طرف نیستیم.
ما با ترکیبی از این مسائل روبه‌رو هستیم:
Geospatial Search
+
Real-Time Location Updates
+
Distributed Systems
+
Matching Optimization

🗺 ایده‌ی اول: نقشه را به Cell تقسیم کنیم
به‌جای اینکه تمام راننده‌ها را در یک لیست بزرگ نگه داریم، نقشه را به بخش‌های کوچک‌تر تقسیم می‌کنیم.
برای مثال:
+---------+---------+---------+
| Cell A | Cell B | Cell C |
+---------+---------+---------+

حالا هر راننده در یک Cell قرار می‌گیرد.
وقتی مسافر در Cell E درخواست سفر می‌دهد، لازم نیست تمام راننده‌های شهر بررسی شوند.
ابتدا این بخش‌ها را بررسی می‌کنیم:
Cell E
Cell D
Cell F
و Cellهای نزدیک دیگر

این کار تعداد Candidateها را بسیار کمتر می‌کند.
اما یک سؤال مهم وجود دارد:
این Cellها را چطور بسازیم؟
⬡ ءH3؛ سیستم مکانی Uber

ءUber برای کارهای جغرافیایی خودش، سیستم H3 را توسعه داد و Open Source کرد. H3 مخفف این عبارت است:
Hexagonal Hierarchical Geospatial Index

یعنی یک سیستم Index مکانیِ سلسله‌مراتبی که جهان را به Cellهای شش‌ضلعی تقسیم می‌کند.
به‌جای اینکه فقط با Latitude و Longitude کار کنیم، مختصات را به یک شناسه‌ی مکانی تبدیل می‌کنیم.
مثلاً به‌صورت مفهومی:
Latitude: 35.7219
Longitude: 51.3347
↓
H3 Cell ID
↓
8a2a1072b59ffff

شناسه‌ی بالا صرفاً یک نمونه از فرمت H3 است، نه شناسه‌ی واقعی یک راننده.
در کد رسمی H3 نیز می‌توان مختصات جغرافیایی را به یک Cell تبدیل کرد:
latLngToCell(latitude, longitude, resolution)


🔎 هنگام درخواست سفر چه اتفاقی می‌افتد؟

فرض کن مسافر در Cell E قرار دارد.
سیستم می‌تواند ابتدا راننده‌های همین Cell را بررسی کند:
Search(Cell E)

اگر راننده‌ی مناسب پیدا نشد، جست‌وجو را به Cellهای اطراف گسترش می‌دهد:
Search(Cell E)
Search(Neighbors of E)
Search(Neighbors of Neighbors)

به این ترتیب، سیستم به‌جای بررسی تمام شهر، یک ناحیه‌ی محدود را بررسی می‌کند.
اما اینجا یک نکته‌ی مهم وجود دارد:
نزدیک‌ترین راننده الزاماً بهترین راننده نیست
فرض کن دو راننده داریم:
Driver A:
فاصله‌ی مستقیم: 800 متر
اما پشت رودخانه است

Driver B:
فاصله‌ی مستقیم: 1.2 کیلومتر
اما از مسیر مستقیم و خلوت می‌تواند برسد

از نظر فاصله‌ی هندسی: A بهتر است

اما از نظر زمان رسیدن: B ممکن است بهتر باشد
خود Uber نیز توضیح داده که در ابتدا Matching را با این سؤال انجام می‌داد:
چه کسی از همه نزدیک‌تر است؟

اما بعد مشخص شد که «نزدیک‌ترین» همیشه به معنی «سریع‌ترین برای رسیدن» نیست.
عواملی مثل:
ترافیک؛
پل‌ها و بزرگراه‌ها؛
رودخانه‌ها؛
خیابان‌های یک‌طرفه؛
مسیر واقعی رانندگی؛
و زمان رسیدن
می‌توانند نتیجه را تغییر دهند.
بنابراین فرآیند واقعی چیزی شبیه این است:
1. پیدا کردن راننده‌های مکانیِ نزدیک
2. حذف راننده‌های نامعتبر
3. تخمین زمان رسیدن
4. بررسی محدودیت‌ها و شرایط Matching
5. انتخاب Assignment مناسب
🗄 10 قابلیت کمترشناخته‌شده SQL که هر Developer باید بداند(پارت 3️⃣)


📊 4. ءGROUPING SETS، ROLLUP و CUBE

یک Report اغلب به چندین سطح از Summary به‌صورت هم‌زمان نیاز دارد: مجموع‌ها بر اساس Carrier و Status، Subtotalهای هر Carrier و یک Grand Total.
روش ساده این است که چند Query را با استفاده از UNION ALL به یکدیگر متصل کنیم.
ءGROUPING SETS، ROLLUP و CUBE تمام این سطوح را در یک Query تولید می‌کنند.
SELECT
carrier,
status,
COUNT(*) AS shipment_count,
SUM(si.quantity) AS total_quantity
FROM shipments.shipments s
LEFT JOIN shipments.shipment_items si
ON s.id = si.shipment_id
GROUP BY GROUPING SETS (
(carrier, status), -- by carrier & status
(carrier), -- subtotal by carrier
(status), -- subtotal by status
() -- grand total
);

SELECT
carrier,
status,
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS shipment_count
FROM shipments.shipments
GROUP BY ROLLUP (
carrier,
status,
DATE_TRUNC('month', created_at)
);

ءQuery اول دقیقاً Groupingهایی را فهرست می‌کند که به آن‌ها نیاز دارید: بر اساس Carrier و Status، فقط بر اساس Carrier، فقط بر اساس Status و () خالی برای Grand Total.
ءROLLUP در Query دوم، شکل کوتاه‌شده‌ای برای Subtotalهای سلسله‌مراتبی است: Carrier، سپس Carrier و Status، بعد Carrier و Status و Month، و در نهایت Total.
ءCUBE تمام ترکیب‌های ممکن از Columnها را تولید می‌کند.
یک Query جایگزین چهار Query می‌شود و Database سطوح مختلف را در یک Pass محاسبه می‌کند، به‌جای اینکه Table را چندین بار Scan کند.
این قابلیت‌ها بخشی از Standard SQL هستند و در PostgreSQL، SQL Server و Oracle کار می‌کنند.

🧮 5. ءFILTER Clause در Aggregateها

اغلب لازم است فقط Rowهایی را Count یا Sum کنید که یک شرط مشخص را دارند و نتیجه‌ی آن‌ها را به‌صورت جداگانه و کنار هم نمایش دهید.
عبارت FILTER یک شرط را روی یک Aggregate مشخص اعمال می‌کند؛ بنابراین هر Aggregate یک Subset متفاوت را Count می‌کند — همه در یک Row و در یک Pass روی داده‌ها.
SELECT
carrier,
COUNT(*) AS total_shipments,
COUNT(*) FILTER (
WHERE status = 'delivered'
) AS delivered_count,
COUNT(*) FILTER (
WHERE status = 'in_transit'
) AS in_transit_count,
COUNT(*) FILTER (
WHERE status = 'pending'
) AS pending_count,
SUM(si.quantity) FILTER (
WHERE status = 'delivered'
) AS delivered_quantity,
SUM(si.quantity) FILTER (
WHERE status = 'pending'
) AS pending_quantity
FROM shipments.shipments s
LEFT JOIN shipments.shipment_items si
ON s.id = si.shipment_id
GROUP BY carrier;

هر COUNT(*) FILTER (WHERE ...) فقط Rowهای مطابق با شرط را Count می‌کند؛ بنابراین برای هر Carrier، تعداد Shipmentهای Delivered، In-Transit و Pending را در Columnهای جداگانه دریافت می‌کنید.
عبارت زیر:
COUNT(*) FILTER (WHERE status = 'delivered')

خواناتر از روش قدیمی استفاده از CASE است:
SUM(
CASE
WHEN status = 'delivered' THEN 1
ELSE 0
END
)

هدف و منظور عبارت FILTER بسیار واضح‌تر است.

📌 نکته: FILTER توسط PostgreSQL پشتیبانی می‌شود. SQL Server و MySQL این قابلیت را ندارند؛ در آن Databaseها باید از CASE داخل Aggregate استفاده کنید، مانند:

COUNT(
CASE
WHEN status = 'delivered' THEN 1
END
)
یه اشتباه رایج توی طراحی سیستم:
همه‌چیز باید Real-Time باشه.
کاربر چیزی تغییر میده،همه‌جا باید همون لحظه تغییر کنه.
ولی واقعاً لازمه؟
فرض کن کاربر Profile خودش رو تغییر داده.
آیا Notification Service باید همان میلی‌ثانیه تغییر رو ببینه؟
آیا Analytics باید همان لحظه Update بشه؟
آیا Search Index باید دقیقاً همان لحظه تغییر کنه؟
شاید نه.
گاهی: Eventual Consistency نه یک مشکل، بلکه یک تصمیم مهندسیه.
اگر ۲ ثانیه تأخیر قابل‌قبوله، لازم نیست برای ۲ ثانیه تأخیر، کل سیستم رو به هم وابسته کنیم.
هر وابستگی Sync یعنی:
ءLatency بیشتر
ءFailure بیشتری
ءCoordination بیشتر
گاهی سیستم بهتر، سیستمی نیست که همه‌چیز رو سریع‌تر Sync می‌کنه.
سیستمیه که می‌دونه کجا لازم نیست Sync باشه.

🔖هشتگ‌ها:
#SystemDesign #Architecture #SoftwareEngineering #DotNet
📩 سیستم پیامک در مقیاس بزرگ؛ چرا همه‌ی پیامک‌ها نباید در یک صف باشند؟ (Part1️⃣)

فرض کن ساعت ۵ عصر است.
یک سیستم بانکی باید هم‌زمان این پیام‌ها را ارسال کند:
🔐 پیامک OTP برای ورود کاربران
💳 پیامک برداشت و تراکنش مالی
📦 پیامک وضعیت سفارش
📢 پیامک تبلیغاتی
📰 پیامک اطلاع‌رسانی عمومی
حالا تصور کن یک کمپین تبلیغاتی شروع شده و ۷ میلیون پیامک وارد سیستم شده است.
اگر همه‌ی پیامک‌ها را داخل یک صف قرار دهیم، چه اتفاقی می‌افتد؟
[Marketing 1]
[Marketing 2]
...
[Marketing 7,000,000]
[OTP Login]

کاربر منتظر ورود به حسابش است، اما پیامک OTP او پشت میلیون‌ها پیامک تبلیغاتی گیر کرده.
از دید سیستم، صف هنوز در حال کار است.
اما از دید کاربر:
سیستم خراب است! 😐

اینجا باید از Priority Queue استفاده کنیم.

🎯 ایده‌ی اصلی Priority Queue
در صف معمولی، پیام‌ها بر اساس زمان ورود پردازش می‌شوند:
First In → First Out

اما در Priority Queue، هر پیام علاوه بر اطلاعات خودش، یک سطح اهمیت هم دارد:
{
"messageId": "msg-123",
"type": "OTP",
"priority": "Critical"
}

سیستم به‌جای اینکه فقط بپرسد:
کدام پیام زودتر وارد شده؟

می‌پرسد:
کدام پیام مهم‌تر است و باید زودتر پردازش شود؟

برای مثال:
Priority 0 → Critical
Priority 1 → High
Priority 2 → Normal
Priority 3 → Low

نکته‌ی مهم:
ءPriority Queue فقط ترتیب پردازش را مشخص می‌کند؛ ارسال واقعی همچنان به ظرفیت Provider، محدودیت شبکه و وضعیت مقصد وابسته است.


🧩 یک صف یا چند صف؟
برای پیاده‌سازی Priority Queue دو روش اصلی وجود دارد.
❗️روش اول: یک صف با Priority
در این روش همه‌ی پیام‌ها در یک Queue هستند، اما هر پیام Priority دارد:
Queue
├── Message A - Priority 3
├── Message B - Priority 1
├── Message C - Priority 2
└── Message D - Priority 0

ءConsumer باید پیام‌ها را بر اساس Priority دریافت کند.
✅مزیت:
مدیریت ساده‌تر
یک مسیر پردازش
مناسب برای سیستم‌های کوچک‌تر
❌مشکل:
کنترل ظرفیت هر سطح دشوارتر می‌شود.
ممکن است پیام‌های مهم وارد Consumerهای اشغال‌شده توسط پیام‌های کم‌اهمیت شوند.
رفتار واقعی به نوع Broker و تنظیمات Consumer وابسته است.
در RabbitMQ، Priority Queue وجود دارد؛ اما مستندات رسمی آن تأکید می‌کنند که اگر Consumer مقدار زیادی پیام را با prefetch دریافت کرده باشد، پیام Priority بالا ممکن است مجبور شود پشت پیام‌های کم‌اهمیتی که قبلاً به Consumer تحویل داده شده‌اند منتظر بماند.
پس این تنظیم مهم است:
Consumer Prefetch

اگر Prefetch بیش از حد بزرگ باشد، Broker تعداد زیادی پیام را زودتر به Consumer تحویل می‌دهد و فرصت اولویت‌بندی کاهش پیدا می‌کند.
❗️روش دوم: چند صف جداگانه
در سیستم‌های مهم‌تر، معمولاً Priorityها را به Queueهای جدا تقسیم می‌کنیم:
sms-critical
sms-high
sms-normal
sms-low

سپس Dispatcher تصمیم می‌گیرد از کدام صف پیام بردارد.
این مدل کنترل بیشتری می‌دهد و می‌توان برای هر صف Consumer یا ظرفیت جداگانه داشت.

⚠️ مشکل خطرناک: Starvation
فرض کن سیستم همیشه پیامک‌های Critical دریافت می‌کند.
Dispatcher هم همیشه این منطق را اجرا می‌کند:
تا وقتی Critical خالی نشده:
فقط Critical را پردازش کن

در این شرایط، پیامک‌های Low ممکن است هیچ‌وقت پردازش نشوند.
به این مشکل می‌گوییم:
Starvation

یعنی یک گروه از کارها دائماً منابع دریافت نمی‌کنند.
مثلاً:
Critical Queue:
همیشه پر است

Low Queue:
هیچ‌وقت نوبتش نمی‌رسد

این رفتار برای OTP شاید مناسب باشد، اما برای پیامک‌های عادی می‌تواند باعث شود:
پیام‌ها بیش از حد قدیمی شوند؛
کمپین‌ها بی‌ارزش شوند؛
هزینه‌ی نگهداری Queue افزایش پیدا کند؛
و Backlog دائماً رشد کند.

🛠 راه‌حل: Weighted Fair Scheduling
به‌جای اینکه همیشه فقط صف Critical را مصرف کنیم، برای هر صف سهمی تعیین می‌کنیم.
مثلاً:
Critical → 70%
High → 20%
Normal → 8%
Low → 2%

یعنی Dispatcher تلاش می‌کند بیشتر ظرفیت را به پیام‌های مهم بدهد، اما صف‌های دیگر را کاملاً گرسنه نگذارد.
یک مدل ساده:
در هر 100 پیام:

70 پیام از Critical
20 پیام از High
8 پیام از Normal
2 پیام از Low

⏳ ءAging؛ اولویت پیام قدیمی را افزایش بده
یک راه دیگر برای جلوگیری از Starvation، تکنیک Aging است.
ایده:
هرچه یک پیام بیشتر منتظر بماند، اولویت مؤثر آن بیشتر شود.

مثلاً:
Marketing Message:
Priority = Low

بعد از چند دقیقه:
Effective Priority = Normal

و اگر باز هم پردازش نشد:
Effective Priority = High

این فرمول فقط برای توضیح مفهوم است؛ در سیستم واقعی باید مراقب باشیم Aging باعث نشود پیام‌های قدیمی تبلیغاتی ناگهان OTPهای جدید را کنار بزنند.
برای همین معمولاً Priorityهای امنیتی و تراکنشی قوانین سخت‌گیرانه‌تری دارند.
⚖️ ءOptimistic Locking vs Pessimistic Locking

فرض کن دو کاربر هم‌زمان موجودی یک کیف پول را تغییر می‌دهند.
موجودی اولیه:
Balance = 1000

کاربر اول می‌خواهد 700 تومان برداشت کند.
کاربر دوم هم‌زمان می‌خواهد 500 تومان برداشت کند.
اگر هر دو درخواست مقدار 1000 را بخوانند:
User A reads: 1000
User B reads: 1000

User A writes: 300
User B writes: 500

نتیجه:
Final Balance = 500

اما در واقع باید فقط یکی از برداشت‌ها موفق شود؛ چون موجودی برای هر دو کافی نیست.
این مشکل یکی از نمونه‌های معروف Race Condition و Lost Update است.
اینجا دو رویکرد مهم داریم:
Optimistic Locking
Pessimistic Locking


🟢 ءOptimistic Locking چیست؟
در Optimistic Locking فرض می‌کنیم برخورد هم‌زمانی معمولاً کم است.
پس هنگام خواندن داده، آن را قفل نمی‌کنیم.
به‌جای قفل‌کردن، هنگام ذخیره بررسی می‌کنیم:
آیا داده‌ای که من خواندم هنوز همان نسخه‌ی قبلی است؟

اگر در این فاصله شخص دیگری داده را تغییر داده باشد، عملیات شکست می‌خورد و باید:
دوباره داده را بخوانیم؛
تصمیم را از نو محاسبه کنیم؛
یا خطای Conflict به کاربر بدهیم.
مثال با Version
فرض کن رکورد حساب این‌طور است:
AccountId = 10
Balance = 1000
Version = 5

کاربر A رکورد را می‌خواند:
Balance = 1000
Version = 5

کاربر B هم همین نسخه را می‌خواند:
Balance = 1000
Version = 5

کاربر A ذخیره می‌کند:
UPDATE Accounts
SET Balance = 300,
Version = 6
WHERE AccountId = 10
AND Version = 5;

این Query موفق می‌شود.
حالا کاربر B می‌خواهد ذخیره کند:
UPDATE Accounts
SET Balance = 500,
Version = 6
WHERE AccountId = 10
AND Version = 5;

اما دیگر رکوردی با Version = 5 وجود ندارد.
پس:
Affected Rows = 0

یعنی:
داده در فاصله‌ی خواندن تا ذخیره تغییر کرده است.

این همان Optimistic Concurrency Conflict است.

🧠 نکته‌ی مهم
ءOptimistic Locking الزاماً به معنی استفاده از یک ستون به نام Version نیست.
می‌توان از موارد زیر هم استفاده کرد:
ءrowversion در SQL Server؛
ءTimestamp یا Version Number؛
مقدار Hash؛
بررسی مقدار قبلی چند ستون؛
شرط روی UpdatedAt؛
ءConcurrency Tokenء در EF Core.
اما برای تشخیص Conflict، باید یک مقدار قابل‌اعتماد برای مقایسه داشته باشیم.

🟦 ءOptimistic Locking
در SQL Server می‌توان از rowversion استفاده کرد:
public sealed class Account
{
public int Id { get; set; }

public decimal Balance { get; set; }

public byte[] RowVersion { get; set; } = [];
}

پیکربندی:
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Account>()
.Property(x => x.RowVersion)
.IsRowVersion();
}

حالا EF Core هنگام Update، مقدار RowVersion قبلی را در شرط قرار می‌دهد.
اگر شخص دیگری رکورد را تغییر داده باشد، EF Core معمولاً با این Exception مواجه می‌شود:
DbUpdateConcurrencyException

🔴 ءPessimistic Locking چیست؟
در Pessimistic Locking فرض می‌کنیم برخورد هم‌زمانی محتمل است.
پس از همان ابتدا داده را قفل می‌کنیم.
یعنی:
تا وقتی من در حال کار روی این رکورد هستم، دیگران نباید بتوانند آن را به شکل ناسازگار تغییر دهند.

مثلاً:
Transaction A:
Lock Account 10
Read Balance = 1000
Withdraw 700
Commit
Release Lock

Transaction B:
Wait for Account 10
Read Balance = 300
Withdraw 500
Reject

در این روش، کاربر دوم باید منتظر آزادشدن Lock بماند.
مثال SQL Server با UPDLOCK
یک الگوی رایج در SQL Server:
BEGIN TRANSACTION;

SELECT Balance
FROM Accounts WITH (UPDLOCK, ROWLOCK)
WHERE Id = 10;

-- Validate balance
-- Update balance

UPDATE Accounts
SET Balance = Balance - 700
WHERE Id = 10;

COMMIT;

ءUPDLOCK باعث می‌شود SQL Server هنگام خواندن، قفل Update بگیرد تا عملیات تغییر هم‌زمان کنترل شود.
اما باید دقت کرد:
ءLock تا پایان Transaction باقی می‌ماند؛
ءTransaction باید کوتاه باشد؛
ءLock طولانی می‌تواند باعث Blocking شود؛
چند Lock ناسازگار می‌توانند Deadlock ایجاد کنند.
🎯 مثال واقعی: ویرایش پروفایل کاربر
فرض کن دو کاربر یا دو Tab، پروفایل یک نفر را باز کرده‌اند.
نسخه‌ی اولیه:
Name = Milad
Version = 3

Tab A:
Name = Milad Heidarpour
Version = 3

Tab B:
Name = Milad Developer
Version = 3

اگر Tab A زودتر ذخیره کند:
Version = 4

Tab B دیگر نباید بی‌خبر تغییرات Tab A را Overwrite کند.
با Optimistic Locking:
Tab B → 409 Conflict

و UI می‌تواند بگوید:
این اطلاعات توسط کاربر دیگری تغییر کرده است. لطفاً داده‌ی جدید را بررسی کنید.
یه چیزی که توی Code Review دیر فهمیدیم:
ءPR بزرگ فقط Review کردنش سخت نیست؛ فهمیدنش سخته.
وقتی یک PR شامل:
ءFeature جدید
ءRefactoring
تغییر Database
و چند Bug Fix
باشه، Reviewer دیگه نمی‌دونه دقیقاً باید دنبال چی بگرده.
حتی اگر همه‌چیز درست باشه،
هزینه‌ی فهمیدن تغییر بالا میره.
گاهی یک PR کوچک‌تر، با Code کمتر،
ارزش بیشتری از یک PR بزرگ و «کامل» داره.
چون هدف Code Review فقط پیدا کردن Bug نیست.
باید بتونی بفهمی چه چیزی تغییر کرده، چرا تغییر کرده، و چه چیزی ممکنه خراب بشه.
ءPriority به‌تنهایی کافی نیست؛ Provider هم محدودیت دارد

فرض کن Queue ما آماده است، اما SMS Provider فقط اجازه می‌دهد:
100 پیامک در ثانیه

ارسال کنیم.
اگر Dispatcher با سرعت ۱۰ هزار پیامک در ثانیه به Provider درخواست بفرستد، اتفاق‌های بدی رخ می‌دهد:
خطای Rate Limit ،Timeout،Retry زیاد ،فشار روی شبکه ،افزایش Backlog و در نهایت Retry Storm
پس بعد از Priority Queue به یک Rate Limiter نیاز داریم:
ءRate Limiter باید ظرفیت واقعی Provider را رعایت کند.
برای مثال:
Provider Limit:
100 SMS/sec

Dispatcher:
بیشتر از 100 ارسال در ثانیه انجام ندهد

اما اگر OTP در صف باشد، نباید با افزایش Priority، محدودیت Provider را دور بزنیم. Priority یعنی:
این پیام زودتر انتخاب شود.

نه اینکه:
این پیام بدون توجه به ظرفیت Provider ارسال شود.


🔁 ءRetry را داخل همان صف اصلی نریزیم
فرض کن ارسال یک پیامک شکست می‌خورد.
راه ساده اما خطرناک:
Send Failed
↓
Put back into Main Queue

اگر Provider مشکل داشته باشد، پیام دائماً از Queue خارج و دوباره وارد آن می‌شود.
راه بهتر، داشتن Retry Policy است:
Attempt 1:
بعد از 1 ثانیه

Attempt 2:
بعد از 5 ثانیه

Attempt 3:
بعد از 30 ثانیه

در عمل باید از:
Exponential Backoff
Jitter
Maximum Attempts
Error Classification
Provider Retry-After
استفاده شود.
همه‌ی خطاها هم نباید Retry شوند.
مثلاً:
Temporary Timeout:
احتمالاً Retry شود

Rate Limit:
با Backoff و رعایت Retry-After

Invalid Phone Number:
Retry نکن

Blocked Destination:
Retry نکن

پیام‌های شکست‌خورده‌ی موقت بهتر است وارد صفی مانند این شوند:
sms-retry

و پیام‌هایی که پس از تعداد مشخصی تلاش همچنان شکست خورده‌اند، به اینجا منتقل شوند:
sms-dead-letter

☠️ ءDead Letter Queue
اگر یک پیام بعد از چند بار تلاش پردازش نشود، نباید برای همیشه در صف اصلی بماند.
مثلاً:
Max Attempts = 5

بعد از پنجمین شکست:
Main Queue
↓
Dead Letter Queue

ءDLQ برای این موارد مفید است:
بررسی علت شکست
اصلاح داده‌ی خراب
ءRetry دستی
گزارش‌گیری
جلوگیری از گیرکردن صف اصلی
اما DLQ نباید تبدیل به سطل زباله شود.
باید برای آن:
Monitoring
Alert
Retention Policy
و فرآیند بررسی داشته باشیم.
🧱 ءDuplicate؛ آیا یک پیامک ممکن است دوبار ارسال شود؟ بله.
فرض کن Provider پیامک را دریافت کرده، اما پاسخ موفقیت به سیستم ما نرسیده است:
Our Service → Provider
│
├── SMS sent
└── Response lost

سیستم ما فکر می‌کند ارسال شکست خورده و دوباره Retry می‌کند:
Retry → Provider

حالا ممکن است کاربر دو پیامک دریافت کند.
برای کاهش این مشکل باید از یک شناسه‌ی یکتا استفاده کنیم:
{
"messageId": "sms-8f91",
"idempotencyKey": "otp-login-user-42-request-981"
}

اما یک نکته‌ی مهم:
داشتن Idempotency Key در سیستم خودمان، تضمین نمی‌کند Provider نیز ارسال را دقیقاً یک‌بار انجام دهد.

این موضوع به قابلیت‌های خود Provider وابسته است.
پس باید:
وضعیت ارسال را ذخیره کنیم؛
شناسه‌ی پیام را به Provider منتقل کنیم، اگر پشتیبانی می‌شود؛
ءCallback و Delivery Report را مدیریت کنیم؛
و Duplicate احتمالی را در طراحی در نظر بگیریم.
⏱️ پیامک قدیمی همیشه ارزش ارسال ندارد
فرض کن پیامک OTP مربوط به ۲۰ دقیقه قبل هنوز در Queue است.
ارسال آن دیگر فایده‌ای ندارد.
یا پیامک وضعیت سفارش مربوط به سفارشی است که لغو شده.
پس هر پیام باید اطلاعاتی مانند این داشته باشد:
{
"messageId": "msg-123",
"priority": "Critical",
"createdAt": "2026-09-15T17:00:00Z",
"expiresAt": "2026-09-15T17:01:00Z",
"attempt": 0
}

ءDispatcher قبل از ارسال بررسی می‌کند:
اگر now > expiresAt:
پیام را منقضی کن
ارسال نکن

این موضوع برای جلوگیری از Stale Work مهم است.
گاهی پردازش‌کردن یک پیام قدیمی بدتر از پردازش‌نکردن آن است.
🎯 نتیجه‌گیری
در سیستم بزرگ، Priority Queue فقط به معنی این نیست که:
پیام مهم‌تر را زودتر از صف بردار.

طراحی درست باید این مسائل را هم‌زمان حل کند:
Priority
+
Fairness
+
Rate Limiting
+
Retry
+
Idempotency
+
Expiration
+
Dead Letter Queue
+
Observability

پس معماری مناسب برای سیستم پیامک بلکه چیزی شبیه این است:
Application
↓
Classification
↓
Priority Queues
↓
Fair Dispatcher
↓
Provider Rate Limiter
↓
SMS Provider
↓
Delivery Tracking

و مهم‌ترین تصمیم مهندسی:
پیامک OTP و پیامک تبلیغاتی نباید فقط به خاطر اینکه هر دو «SMS» هستند، با یک SLA و یک مسیر پردازش مدیریت شوند.

چون در سیستم‌های واقعی، همه‌ی پیام‌ها ارزش یکسانی ندارند. 📩
🗄 10 قابلیت کمترشناخته‌شده SQL که هر Developer باید بداند(پارت 4️⃣)


🔄 6. ءUPSERT یا INSERT ... ON CONFLICT

اگر Row جدید است، آن را Insert کن؛ اگر از قبل وجود دارد، آن را Update کن.
این یک نیاز رایج است که معمولاً به یک SELECT، یک IF و دو مسیر کدنویسی جداگانه نیاز دارد.
ءUPSERT تمام این کارها را در یک Statement اتمیک انجام می‌دهد و بین Check و Write نیز Race Condition ایجاد نمی‌شود.
ALTER TABLE shipments.shipments
ADD CONSTRAINT shipments_number_unique
UNIQUE (number);

INSERT INTO shipments.shipments (
id,
number,
order_id,
address_street,
address_city,
address_zip,
carrier,
receiver_email,
status,
created_at,
updated_at
)
VALUES (
'550e8400-e29b-41d4-a716-446655440000',
'SH-2024-001',
'ORD-2024-001',
'123 Main St',
'New York',
'10001',
'FedEx',
'customer@example.com',
'pending',
NOW(),
NOW()
)
ON CONFLICT (number) DO UPDATE SET
carrier = EXCLUDED.carrier,
status = EXCLUDED.status,
updated_at = GREATEST(
shipments.updated_at,
EXCLUDED.updated_at
);

ابتدا یک Unique Constraint روی Column مربوط به number اضافه می‌کنیم؛ یعنی همان Columnای که Conflict بر اساس آن شناسایی می‌شود.
سپس INSERT ... ON CONFLICT (number) DO UPDATE تلاش می‌کند Row را Insert کند. اگر Rowای با همان number از قبل وجود داشته باشد، به‌جای Insert، آن را Update می‌کند.
ءPseudo-table مربوط به EXCLUDED مقادیری را نگه می‌دارد که تلاش کرده‌اید Insert کنید.
بنابراین:
carrier = EXCLUDED.carrier

یعنی:
«از مقدار Carrier جدید استفاده کن.»
عبارت زیر:
GREATEST(
shipments.updated_at,
EXCLUDED.updated_at
)

مقدار جدیدتر از بین دو Timestamp را نگه می‌دارد.
یک Statement، بدون Duplicate Row و بدون Race Condition از نوع Read-Modify-Write بین Callerهای هم‌زمان.
نکته: این Syntax مربوط به PostgreSQL است. در SQL استاندارد، مانند SQL Server و Oracle، از Statement مربوط به MERGE استفاده می‌شود. در MySQL نیز از عبارت زیر استفاده می‌شود:
INSERT ... ON DUPLICATE KEY UPDATE


🧩7. پشتیبانی از JSON

همه‌ی داده‌ها رابطه‌ای نیستند. گاهی لازم است یک payload منعطف و نیمه‌ساخت‌یافته ذخیره کنید؛ مثلاً یک event، بدنه‌ی یک webhook یا یک blob مربوط به تنظیمات.
ءPostgreSQL داده‌های JSON را به‌صورت native در نوع JSONB ذخیره می‌کند و اجازه می‌دهد داخل آن query بزنید؛ بنابراین برای JSONهای موردی، نیازی به یک document database جداگانه ندارید. همچنین مجبور نیستید JSON را به‌صورت string ذخیره کنید و پردازش بیشتر آن را به کد backend بسپارید؛ روشی که تمام قابلیت‌های index را از دست می‌دهد.
CREATE TABLE shipments.events (
id SERIAL PRIMARY KEY,
payload JSONB NOT NULL
);

-- Insert sample data
INSERT INTO shipments.events (payload)
VALUES
('{"type":"click","coordinates":[{"x":10,"y":20},{"x":15,"y":25}]}'),
('{"type":"hover","coordinates":[{"x":5,"y":30}]}'),
('{"type":"scroll","coordinates":[{"x":0,"y":100},{"x":0,"y":200},{"x":0,"y":300}]}');

-- Extract simple JSON fields
SELECT
payload ->> 'type' AS event_type,
payload -> 'coordinates' -> 0 ->> 'x' AS first_x,
payload -> 'coordinates' -> 0 ->> 'y' AS first_y
FROM shipments.events;

جدول events یک payload از نوع JSONB ذخیره می‌کند. سپس query وارد آن می‌شود:
->> یک مقدار را به‌صورت text استخراج می‌کند.
-> یک JSON object یا یک عنصر تو‌در‌تو از یک array را استخراج می‌کند.
بنابراین عبارت زیر، مقدار x از اولین coordinate را می‌خواند:
payload -> 'coordinates' -> 0 ->> 'x'

ءJSONB به‌صورت binary و parse‌شده ذخیره می‌شود و می‌توان روی آن index ساخت؛ بنابراین می‌توانید بدون scan کردن کل documentها، روی آن‌ها filter و extract انجام دهید.
📝 نکته: SQL Server برای query زدن روی JSON از JSON_VALUE و OPENJSON استفاده می‌کند؛ استاندارد SQL نیز JSON_TABLE را اضافه می‌کند؛ قابلیتی که در Oracle، MySQL و PostgreSQL 17+ وجود دارد و یک JSON array را مستقیماً به ردیف‌های relational تبدیل می‌کند.