Độ mới của cache TTL phụ thuộc lúc bắt đầu tính giờ
So sánh TTL từ lúc chọn và lúc nhận dữ liệu, kiểm tra biên hết hạn và truyền tuổi phản hồi qua ví dụ cache HTTP cụ thể.

TTL mười giây có thể mang nghĩa “dùng lại giá trị trong mười giây kể từ khi nạp xong”. Nó cũng có thể mang nghĩa “chỉ dùng lại đến khi đủ mười giây kể từ lúc quan sát nguồn”. Hai chính sách cho kết quả khác nhau nếu việc nạp mất thời gian. Bạn cần chọn sự kiện bắt đầu tính giờ trước khi xem TTL là một giới hạn về độ mới của dữ liệu.
Bài này dành cho kỹ sư ứng dụng có thể đọc một hàm Python ngắn và muốn rà soát quy tắc hết hạn của cache. Ta dùng dòng thời gian giả định với các mốc nguyên, không dùng số đo độ trễ dịch vụ. Mô hình đầu giả sử lần nạp chọn giá trị mới nhất tại nguồn ngay lúc bắt đầu, rồi trả đúng giá trị đó sau một khoảng chờ. Giả thiết này giúp kiểm tra được phép tính tuổi dữ liệu; nguồn là bản sao được cập nhật bất đồng bộ cần một cam kết chặt hơn trước khi áp dụng cùng kết luận.
Tách lúc chọn giá trị, lúc nhận và lúc cập nhật
Xét một khóa cấu hình. Tại thời điểm 0, lần đọc chọn giá trị old. Tại thời điểm 2, nguồn có thẩm quyền ghi nhận new. Lần đọc đang chờ vẫn trả old tại thời điểm 8. Chuỗi sự kiện này có thể xảy ra khi kết quả đã được chốt trước lúc phản hồi đi hết đường truyền hoặc hoàn tất xử lý.
TTL mười đơn vị tính từ lúc nhận tạo hạn chót 18. Nếu tính từ lúc chọn giá trị, hạn chót là 10. Tại thời điểm 13, chính sách tính từ lúc nhận vẫn cho phép dùng giá trị cũ, dù đã qua mười ba đơn vị kể từ lúc chọn nó. Chính sách tính từ lúc chọn yêu cầu nạp lại hoặc thực hiện một hành động khác đã được quy định rõ.
Cả hai chính sách đều không biết về lần cập nhật ở thời điểm 2. Tại thời điểm 9, cả hai vẫn cho phép old, dù new đã tồn tại. Hết hạn là quy tắc dùng lại, không phải thông báo từ nguồn. Một mục cache còn hạn vẫn có thể khác giá trị hiện tại tại nguồn có thẩm quyền. Khi ai đó gọi cache là “mới”, cần phân biệt họ đang nói đến tuổi còn trong giới hạn cho phép hay trạng thái bằng với lần ghi mới nhất đã hoàn tất.
Khoảng nạp tám đơn vị tiêu thụ một phần quỹ thời gian tính từ lúc chọn. Bắt đầu quỹ mới khi nhận kéo dài thời gian được phép dùng lại cùng giá trị thêm tám đơn vị.
Quy tắc hết hạn nhỏ có thể chạy được
Chương trình Python 3 sau tính điều kiện được dùng cho một giá trị đã nạp. Mọi đầu vào là số nguyên dựng sẵn không âm trong cùng một hệ thời gian giả định. Chương trình từ chối Boolean, dù Python coi kiểu này là kiểu con của số nguyên. Ta tạo Window qua ttl_window; tự gọi hàm khởi tạo của lớp nằm ngoài giao diện đã kiểm tra đầu vào trong ví dụ nhỏ này.
Cửa sổ kết quả ghi lại lúc phản hồi đến và hạn hết hiệu lực. Hỏi một thời điểm trước lúc nhận là lỗi: khi đó chưa có giá trị để phục vụ. Ngay tại hạn chót, giá trị đã hết hạn. So sánh nghiêm ngặt này tránh vô tình cộng thêm một đơn vị thời gian tại mỗi biên.
from dataclasses import dataclass
def tick(value):
if type(value) is not int or value < 0:
raise ValueError("use nonnegative integer ticks")
return value
@dataclass(frozen=True)
class Window:
received: int
expires: int
def usable(self, now):
tick(now)
if now < self.received:
raise ValueError("the value has not arrived yet")
return now < self.expires
def ttl_window(start, finish, ttl, *, from_completion=False):
for value in (start, finish, ttl):
tick(value)
if finish < start or type(from_completion) is not bool:
raise ValueError("invalid load interval or policy")
base = finish if from_completion else start
return Window(finish, base + ttl)
from_start = ttl_window(0, 8, 10)
from_finish = ttl_window(0, 8, 10, from_completion=True)
assert (from_start.expires, from_finish.expires) == (10, 18)
assert from_start.usable(9) and from_finish.usable(9)
assert not from_start.usable(13) and from_finish.usable(13)
assert not from_start.usable(10)
assert not from_finish.usable(18)
assert not ttl_window(0, 12, 10).usable(12)
for policy in (False, True):
assert not ttl_window(0, 8, 0, from_completion=policy).usable(8)
print("deadlines:", from_start.expires, from_finish.expires)
Hãy chạy khối mã phía sau trong cùng tệp. Đây là các ví dụ số học có kết quả xác định nên không cần ngủ chờ hay chỉnh đồng hồ hệ thống. usable chỉ trả lời một câu hỏi về chính sách tuổi. Nó không trả dữ liệu, xóa mục, liên hệ nguồn hay quyết định ảnh hưởng của một lần nạp lại thất bại đối với bên gọi.
Kiểm tra lần nạp lâu đáng được xem kỹ. Nếu chọn giá trị ở mốc 0 nhưng đến mốc 12 mới nhận, quỹ mười đơn vị tính từ lúc chọn đã cạn. Vẫn phục vụ kết quả đó sẽ làm thay đổi cam kết. Ứng dụng có thể thử lại, trả lỗi, xác nhận phiên bản hoặc cho phép dữ liệu cũ theo quy định riêng. Liên tục thử lại một nguồn luôn mất nhiều thời gian nạp hơn quỹ cho phép có thể khiến hệ thống không phục vụ được mà vẫn chẳng tạo ra mục đủ điều kiện. Vì vậy, giảm TTL không phải lúc nào cũng đem lại lợi ích mà không có đánh đổi.
TTL bằng 0 không cho phép dùng lại, kể cả ngay lúc nhận. Nó không có nghĩa “không bao giờ hết hạn”. Một số API cache thực tế gán ý nghĩa đặc biệt cho 0, vì vậy phải kiểm tra cam kết của giao diện trước khi chuyển mô hình thành lời gọi thư viện.
Nêu đúng giới hạn có thể chứng minh
Gọi s là lúc chọn giá trị, f là lúc nhận, T là tuổi cho phép và n là thời điểm quan sát. Ta chỉ xét thời điểm từ lúc nhận trở đi. Thời gian nạp là D = f - s.
Với cách hết hạn tính từ lúc chọn, điều kiện được dùng là n < s + T, tương đương n - s < T. Mọi giá trị được chấp nhận đều có tuổi nhỏ hơn T kể từ lần chọn đó. Với cách tính từ lúc nhận, điều kiện là n < f + T; thay f = s + D cho ta n - s < D + T. Cách sau là một chính sách dùng lại hợp lệ, nhưng không chứng minh được giới hạn tuổi của cách trước khi thời gian nạp lớn hơn 0.
Bắt đầu tính giờ trước lúc chụp trạng thái nguồn có thể thận trọng hơn: thời gian chờ đọc cũng tiêu thụ quỹ. Bắt đầu sau khi trạng thái đã cũ một lúc có thể đánh giá quá lạc quan. Tên hàm hay tên trường created_at không tự cho biết nó đại diện cho sự kiện nào.
Chứng minh không nói gì về việc chính nguồn đã biết giá trị đó từ bao lâu. Nếu bản sao trả dữ liệu vốn đã lỗi thời trước thời điểm 0, tính từ 0 sẽ bỏ qua phần tuổi trước đó. Giả thiết “mới nhất lúc chọn” có vai trò thực sự. Muốn giới hạn tuổi qua nhiều cache hoặc bản sao, cần giữ thông tin tuổi hay xác nhận từ phía trước có ý nghĩa rõ ràng, đồng thời định nghĩa thông tin ấy đang đo điều gì.
Vì sao HTTP truyền tuổi qua các cache
Cache HTTP thể hiện rõ sự phân biệt này. Mục 4.2 của RFC 9111 so sánh tuổi hiện tại của phản hồi với thời gian được coi là mới; chỉ nhận phản hồi không đồng nghĩa tuổi của nó được đặt về 0. Mục 4.2.3 mô tả trường Age là ước lượng thời gian từ lúc nguồn gốc tạo hoặc xác nhận phản hồi, gồm cả thời gian nằm trong cache và thời gian truyền.
Xét một phép tính HTTP giả định riêng. Yêu cầu bắt đầu ở mốc 100 và phản hồi đến ở mốc 108. Date của phản hồi là 105, còn Age là 4. Tuổi biểu kiến là ba giây; tuổi được cung cấp cộng độ trễ phản hồi của yêu cầu này là mười hai giây. Ta dùng giá trị lớn hơn theo cách thận trọng ở mục 4.2.3, được tuổi ban đầu mười hai giây. RFC cũng cho phép dùng trực tiếp giá trị tuổi đã hiệu chỉnh trong những điều kiện mà tài liệu nêu; ví dụ này minh họa chính sách lấy giá trị lớn hơn.
Nếu thời gian được coi là mới là mười giây, phản hồi đã cũ ngay lúc đến. Nếu thay bằng mười lăm giây, nó còn ba giây trước khi tuổi đạt mười lăm. Đây là hai lựa chọn cho cùng ví dụ số học, không phải hai header cùng được áp dụng lên một phản hồi.
Hình theo dõi tuổi ước lượng của phản hồi. Nó không khẳng định TTL cục bộ có thể suy ra thời điểm cập nhật chưa biết tại nguồn.
Hàm sau chỉ thực hiện phép tính số. Các thời điểm đã được phân tích thành số nguyên giây không âm trong các hệ quy chiếu tương thích; yêu cầu, phản hồi và quan sát có thứ tự đúng. Hàm không phân tích header, chọn khóa cache, xét quyền lưu trữ, xử lý bộ xác nhận hay triển khai một cache HTTP.
def http_age(request, response, date, age, now, lifetime):
for value in (request, response, date, age, now, lifetime):
tick(value)
if not request <= response <= now:
raise ValueError("request, response and observation must be ordered")
apparent = max(0, response - date)
corrected = age + response - request
initial = max(apparent, corrected)
current = initial + now - response
return initial, current, current < lifetime
assert http_age(100, 108, 105, 4, 108, 10) == (12, 12, False)
assert http_age(100, 108, 105, 4, 110, 15) == (12, 14, True)
assert http_age(100, 108, 105, 4, 111, 15) == (12, 15, False)
print("HTTP age:", http_age(100, 108, 105, 4, 111, 15))
Thời gian được coi là mới là đầu vào ở đây. Cache HTTP thực tế phải xác định nó theo mục 4.2.1, trong đó phân biệt s-maxage cho cache dùng chung, max-age, Expires và các quy tắc suy đoán phù hợp. Việc có được dùng lại phản hồi hay không còn phụ thuộc những chỉ thị khác. Mục 4.2.4 đặt điều kiện cho việc phục vụ phản hồi đã cũ. Giá trị Boolean của ta là kết quả tính độ mới, không phải quyết định cấp phép đầy đủ để gửi phản hồi.
Chọn đồng hồ có ý nghĩa phù hợp
Để đo khoảng thời gian cục bộ, time.monotonic() của Python không đi lùi và không bị ảnh hưởng bởi việc cập nhật đồng hồ hệ thống. monotonic_ns() trả số nguyên nano giây, tránh mất độ chính xác do số thực dấu phẩy động. Một triển khai cục bộ thực tế có thể lấy mốc bắt đầu, kết thúc và tra cứu từ cùng đồng hồ đó, rồi biểu diễn TTL bằng đơn vị tương ứng.
Mốc gốc của đồng hồ này không được định nghĩa. Đừng lấy số đọc đơn điệu trừ một dấu thời gian HTTP Date rồi gọi kết quả là tuổi biểu kiến. Dấu thời gian theo lịch và khoảng thời gian cần một chiến lược chuyển đổi rõ ràng; giá trị thuộc các gốc đồng hồ không liên quan không thể thay thế cho nhau. Tương tự, đừng lưu một hạn chót cục bộ rồi mặc nhiên cho rằng nó vẫn có nghĩa sau khi môi trường thay đổi. Các ví dụ số tách rõ hệ đồng hồ để có thể xem xét chính sách độc lập với hành vi đồng hồ của nền tảng.
Hết hạn cũng không có nghĩa bộ nhớ được thu hồi ngay. Cache có thể kiểm tra hạn khi truy cập khóa, quét mục hết hạn theo lịch riêng hoặc kết hợp cả hai. Điều đó ảnh hưởng dung lượng chiếm dụng và công việc dọn dẹp. Cam kết liên quan đến bài này là đường đọc có phục vụ mục hết hạn hay không, bất kể các byte của nó còn được cấp phát hay đã được giải phóng.
Kiểm tra một biên trước khi chọn TTL
Bài tập: đổi thời gian nạp từ tám lên mười hai, giữ quỹ tuổi là mười. Dự đoán thời điểm sớm nhất được phép dùng theo mỗi chính sách. Với chính sách tính từ lúc chọn, kết quả trả về này không có thời điểm nào đủ điều kiện; với chính sách tính từ lúc nhận, có thể dùng từ mười hai đến trước hai mươi hai. Trong tình huống riêng tiếp theo, đặt lại thời gian nạp là tám, cho nguồn cập nhật ở mốc 1 và quan sát cache ở mốc 9. Khi đó cả hai chính sách đều cho phép giá trị lỗi thời trong khoảng còn hạn của mình.
Khi rà soát cache, hãy gọi tên sự kiện chốt giá trị, sự kiện bắt đầu quỹ thời gian, đồng hồ dùng để đo và hành động khi quỹ đã cạn. Nếu dữ liệu phía trước vốn có thể đã cũ, hãy đưa điều đó vào cam kết. Những lựa chọn này quyết định TTL hứa điều gì; số giây chỉ điền tham số vào quy tắc đã chọn.