Daily Archives: 17/04/2016

Django – Cache

Website được tạo ra ngày nay là website động, tức là nội dung HTML sẽ được server sinh ra rồi trả về cho người dùng mỗi khi người dùng gửi request đến, khác với website tĩnh là các file HTML đã có sẵn, người dùng request thì chỉ cần trả về file HTML đó thôi. Điều này cũng có nghĩa là website động sẽ tốn nhiều thời gian trả lời hơn so với website tĩnh, và khi lượt truy cập website càng nhiều thì thời gian này càng tăng lên gấp nhiều lần.

Kỹ thuật cache ra đời là để cắt giảm quá trình tính toán của website để có thể cung cấp nội dung cho người dùng một cách nhanh chóng hơn, cache có nhiều loại nhưng nhìn chung thì đều tuân theo thuật toán sau đây:

user gửi request một trang web, tìm xem trang đó đã có cache hay chưa
nếu đã có cache:
    trả về cache
ngược lại:
    tạo cache
    lưu lại trang cache vừa tạo
    trả về trang cache vừa tạo

Django cung cấp sẵn hệ thống cache rất mạnh mẽ, chúng ta sẽ lần lượt đi tìm hiểu.

Thiết lập Cache

Việc thiết lập cache trong Django rất đơn giản, chỉ là cho Django biết cache sẽ được lưu ở đâu thôi bởi vì cache lưu trên RAM sẽ có hiệu suất khác hẳn so với lưu trên file.

Tất cả các thông tin cài đặt cache đều được lưu trong biến CACHE trong file settings.py, mặc định khi tạo project thì thông tin này chưa có, chúng ta phải tự thêm vô.

Lưu cache trên RAM – Memcached

Memcached đúng với cái tên của nó, là lưu nội dung trên bộ nhớ RAM của máy chủ, do đó loại cache này có tốc độ tìm kiếm cũng như trả về nhanh nhất, thích hợp cho các website lớn có lượng truy cập cao, các website như Facebook hay Wikipedia đều dùng loại cache này.

Memcached là một chương trình dạng dịch vụ (tức là chạy ngầm bên dưới hệ điều hành) được cấp một lượng RAM nhất định, cung cấp các hàm cho phép cập nhật dữ liệu trên cache, không đụng chạm gì tới đĩa cứng hoặc cơ sở dữ liệu.

Để thiết lập memcached thì chúng ta cung cấp những thông tin sau:

  • BACKEND: django.core.cache.backends.memcached.MemcachedCache hoặc django.core.cache.backends.memcached.PyLibMCCache.
  • LOCATION: theo cú pháp IP:PORT, trong đó IP là địa chỉ máy lưu cache, port là cổng tương ứng của trình dịch vụ cache.

Ví dụ:

CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
    'LOCATION': '127.0.0.1:11211',
    }
}

Đoạn code trên thiết lập memcache tại máy localhost, tức là lưu trên chính server đó trên port 11211.

Chúng ta cũng có thể cho chạy nhiều trình memcached trên nhiều máy:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': [
        '172.19.26.240:11211',
        '172.19.26.242:11211',
        ]
    }
}

Chỉ cần khai báo thêm IP và Port, ngăn cách nhau bởi dấu phẩy là được, port có thể khác nhau chứ không cần phải giống nhau.

Lưu cache trên cơ sở dữ liệu

Loại này sẽ lưu dữ liệu trong một bảng trên CSDL. Để sử dụng thì chúng ta thiết lập như sau:

  • BACKEND: django.core.cache.backends.db.DatabaseCache
  • LOCATION: tên bảng được dùng để lưu cache trong CSDL, tất nhiên phải là bảng trắng, chưa có gì trong đó

Ví dụ:

CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
    'LOCATION': 'my_cache_table',
    }
}

Sau khi đã khai báo trong file settings.py thì chúng ta phải tạo bảng lưu cache bằng cách chạy lệnh:

python manage.py createcachetable

Django sẽ tự động tạo bảng với tên tương ứng trong biến LOCATION cùng các trường cần thiết để lưu cache.

Lưu cache trong file

Loại cache này sẽ lưu dữ liệu trong file, khi nào cần thì sẽ đọc file. Chúng ta thiết lập như sau:

  • BACKEND: django.core.cache.backends.filebased.FileBasedCache
  • LOCATION: đường dẫn đến file

Ví dụ:

CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
    'LOCATION': 'c:/cache.txt',
    }
}

Đường dẫn file phải là đường dẫn tuyệt đối – tức là phải có tên ổ đĩa cứng.

Các tham số khác

Ngoài 2 tham số bắt buộc là BACKENDLOCATION thì khi thiết lập cache chúng ta còn có các tham số tùy chọn khác như sau:

  • TIMEOUT: thời gian lưu trữ cache, mặc định là 300 giây (5 phút), bạn có thể đưa vào là None và Django sẽ lưu cache vô thời hạn.
  • OPTIONS: danh sách một số tùy chọn cache, bao gồm MAX_ENTRIES là số lượng trang tối đa được phép cache, mặc định là 300; CULL_FREQUENCY là số lượng trang cache bị xóa khi số lượng trang cache đã đạt mức tối đa, tính bằng 1 / CULL_FREQUENCY, ví dụ CULL_FREQUENCY là 2 thì nếu số trang cache đã đạt đến 300 trang thì số lượng trang bị hủy là 150 trang. Nếu thiết lập CULL_FREQUENCY=0 thì xóa toàn bộ cache.
  • KEY_PREFIX: đây là một chuỗi được thêm vào đầu các khóa được lưu trong cache. Chúng ta sẽ tìm hiểu thêm về khóa ở dưới.
  • VERSION: số phiên bản cache sử dụng. Chúng ta cũng sẽ tìm hiểu ở dưới.
  • KEY_FUNCTION: tên hàm thực hiện việc tạo chuỗi key lưu trong cache.

Ví dụ:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': 'c:/cache.txt',
        'TIMEOUT': 60,
        'OPTIONS': {
            'MAX_ENTRIES': 1000
        }
    }
}

Đoạn trên thiết lập cache được lưu trong file cache.txt, thời gian mỗi trang tồn tại là 60 giây, số lượng cache tối đa là 1000.

Nếu chúng ta có lỡ thiết lập sai biến nào thì Django sẽ không báo lỗi mà bỏ qua xem biến khác, do đó khi thiết lập cache chúng ta nên kiểm tra lại cho kỹ.

Cache cả trang

Việc cache cũng có nhiều kiểu, chúng ta có thể cache từng phần hoặc cache nguyên cả trang web, nguyên trang tức là trong toàn bộ website của bạn có trang nào thì cũng đều được cache lại. Để cache cả trang web thì chúng ta khai báo các lớp sau trong biến MIDDLEWARE_CLASSES trong file settings.py:

MIDDLEWARE_CLASSES = [
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
]

Lưu ý là bạn phải khai báo đúng thứ tự như trên thì mới sử dụng được.

Lớp FetchFromCacheMiddleware sẽ lưu lại các trang web mà có mã trả về là 200 và có request được gửi lên bởi phương thức GETHEAD. Các yêu cầu đến trang web có tham số khác nhau thì được cache khác nhau, tức là giả sử chúng ta có một trang liệt kê danh sách sản phẩm được phân trang thì mỗi request với số trang khác nhau sẽ được phân trang khác nhau.

UpdateCacheMiddleware sẽ ghi một số thông tin vào cache như ngày/giờ tạo trang cache, thời gian tồn tại…

Cache từng view

Nếu cache toàn bộ website có hơi thừa thì chúng ta cũng có thể cache từng trang tùy vào từng hàm view. Để cache từng view thì chúng ta dùng hàm cache_page() trong lớp django.views.decorators.cache.

Chúng ta cũng không dùng hàm này như các hàm thông thường mà khai báo trước tên hàm view, ví dụ:

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
    ...

Trước hàm này chúng ta thêm dấu @. Hàm cache_page() nhận 1 tham số là thời gian cache tồn tại tính theo giây, ở trên chúng ta cho thời gian này là 60 * 15, tức là 15 phút, bạn có thể ghi ra số giây rõ ràng luôn chứ không nhất thiết phải dùng biểu thức nhân như vậy.

Cũng giống như cache cả trang, cache trên view cũng phân biệt tham số, tức là trang có URL như localhost:8000/cache-page/1localhost:8000/cache-page/2 sẽ được cache riêng.

Cache template

Ngoài việc cache các hàm view, bạn cũng có thể cache các phần của template. Thẻ {% load cache %} sẽ tải cache về nếu có, cặp thẻ {% cache %}...{% endcache %} sẽ cache lại nội dung bên trong nó trong một khoảng thời gian, thẻ này nhận vào 2 tham số bắt buộc là tên cache và thời gian cache. Ví dụ:

{% load cache %}
{% cache 500 sidebar %}
    ...
{% endcache %}

Cache dữ liệu

Nhiều khi việc cache nguyên cả một trang web là quá thừa thãi, đôi khi còn phản tác dụng. Đối với những trang web có nội dung thay đổi liên tục và mỗi lần tải trang là một lần truy vấn một lượng dữ liệu lớn, chẳng hạn như Facebook, thì việc cache nguyên trang là không tối ưu, thay vào đó chúng ta chỉ nên cache những thứ ít thay đổi như giao diện…

Django cũng cung cấp các hàm cache cấp thấp để hỗ trợ bạn làm việc này, bạn có thể cache bất cứ kiểu dữ liệu nào của Python như string, dictionary, list…

Truy vấn dữ liệu cache

Những dữ liệu mà bạn đã cache lại sẽ được lưu trong đối tượng django.core.cache.caches, đây là một đối tượng tĩnh toàn cục, tức là bạn có thể truy xuất ở bất kỳ đâu, đối tượng này lưu dữ liệu theo dạng dictionary, tức là mỗi phần tử là một cặp khóa-giá trị, chúng ta có thể truy xuất dữ liệu cache như sau:

>>> from django.core.cache import caches
>>> cache1 = caches['key1']
>>> cache2 = caches['key1']
>>> cache1 is cache2
True

Nếu chúng ta truy xuất sai tên khóa thì Django sẽ báo lỗi exception InvalidCacheBackendError.

Thiết lập cache

Lớp django.core.cache cung cấp 2 phương thức để thiết lập và lấy dữ liệu cache là set(key,value,timeout)get(key). Ví dụ:

>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
'hello, world!'

Tham số timeout là tùy chọn, nếu chúng ta không đưa tham số này vào thì Django sẽ sử dụng thông số được thiết lập trong file settings.py. Nếu không tìm thấy dữ liệu thì phương thức get() sẽ trả về đối tượng None.

Nhưng nếu muốn chúng ta có thể thêm tham số default vào phương thức get() và Django sẽ trả về giá trị default nếu không tìm thấy dữ liệu cache:

>>> cache.get('my_key', 'has expired')
'has expired'

Ngoài phương thức set() chúng ta còn có phương thức add() dùng để thiết lập thêm dữ liệu cache. Điểm khác nhau giữa 2 phương thức là set() sẽ cập nhật lại dữ liệu nếu cache đã tồn tại, còn add() thì không:

>>> cache.set('add_key', 'Initial value')
>>> cache.add('add_key', 'New value')
>>> cache.get('add_key')
'Initial value'

Nếu bạn muốn lấy một giá trị mà không biết là đã có trong cache hay chưa, bạn có thể dùng phương thức get_or_set(), đúng như cái tên của nó, là sẽ trả về giá trị nếu đã có, còn nếu chưa có thì tạo mới:

>>> cache.get('my_new_key') # returns None
>>> cache.get_or_set('my_new_key', 'my new value', 100)
'my new value'

Ngoài các dữ liệu thông thường, bạn cũng có thể truyền giá trị là một phương thức nào đó, tất nhiên là phương thức này phải tra về một giá trị nào đó:

>>> import datetime
>>> cache.get_or_set('some-timestamp-key', datetime.datetime.now)
datetime.datetime(2014, 12, 11, 0, 15, 49, 457920)

Nếu bạn muốn lấy nhiều giá tri cùng một lúc thì dùng phương thức get_many(), phương thức này rất hữu ích nếu chúng ta dùng phương pháp cache trên file hoặc trên cơ sở dữ liệu, vì chỉ cần đọc file cache một lần rồi lấy hết dữ liệu ra, không như phương thức get() là mỗi lần chỉ đọc một giá trị:

>>> cache.set('a', 1)
>>> cache.set('b', 2)
>>> cache.set('c', 3)
>>> cache.get_many(['a', 'b', 'c'])
{'a': 1, 'b': 2, 'c': 3}

Tương tự, chúng ta cũng có phương thức set_many() thiết lập nhiều giá trị cùng một lúc:

>>> cache.set_many({'a': 1, 'b': 2, 'c': 3})
>>> cache.get_many(['a', 'b', 'c'])
{'a': 1, 'b': 2, 'c': 3}

Để xóa một giá trị cache nào đó thì chúng ta dùng phương thức delete():

>>> cache.delete('a')

Tương tự với get_many()set_many(), chúng ta có delete_many():

>>> cache.delete_many(['a', 'b', 'c'])

Nếu muốn xóa toàn bộ cache thì dùng phương thức clear():

>>> cache.clear()

Phiên bản cache

Phiên bản ở đây là chúng ta dùng các con số để đánh dấu dữ liệu cache nào thuộc về nhóm nào chứ không phải là phiên bản phần mềm cache của Django hay cái gì đó tương tự 🙂 Mặc định Django sẽ dùng số phiên bản là con số được khai báo trong file settings.py, nhưng chúng ta cũng có thể override lại trong khi viết code bằng cách đưa vào tham số version:

>>> cache.set('my_key', 'hello world!', version=2)
>>> cache.get('my_key')
None
>>> cache.get('my_key', version=2)
'hello world!'

Trong đoạn code trên chúng ta thiết lập khóa my_key với version là 2, khi chúng ta lấy giá trị của khóa này mà không chỉ định rõ version nào thì Django sẽ lấy khóa có version được lưu trong file settings.py, mặc định ở đây trong file settings.py lưu version là 1 nên đoạn code trên trả về đối tượng None.

Chúng ta cũng có thể tăng/giảm giá trị của version thông qua 2 phương thức incr_version()decr_version():

# version = 3
>>> cache.incr_version('my_key') 
>>> cache.get('my_key')
None
>>> cache.get('my_key', version=2)
None
>>> cache.get('my_key', version=3)
'hello world!'