Надшвидкі бази даних. Django and Redis

30 Серпня 2009

Коли виникають великі навантаження на базу данних то ставлять більший сервер, потім ставлять два крутих сервера, далі може ще трішки їх додають, але чим далі - тим все складніше, бо маштабування в звичайних базах данних майже відсутнє. І сьогодні хочу розповісти про key-value бази даних і про приклад їх використання на прикладі блогу зробленого в Django і в якості бази даних використовую Redis. Але зараз вже 6-та година ранку і писати мені лінь, тому дальше буде скрінкаст:

Django and Redis from presidentua on Vimeo.

А знявши скрінкаст побачив, що трішки ранковий голос мене підкачав та й занадто тихо говорю, тому потрібно б розповісти і словами.

Отдже Redis це представник key-value бази даних, тобто такої бази де немає sql, а є лише дві команди set і get, тобто по суті звичайний масив, але через таку простоту база даних працює надзвичайно швидко і дуже легко маштабується і нові сервера доставляються за декілька хвилин. Хоча ці ж обмеження заставляють думати над архітектурою БД набагато більше чим зі звичайною.

Всі ісходники системи можна глянути на github.com/presidentua/Dredis

Отже перейдемо до найбільше цікавого коду, це до файлу views.py, про те нащо потрібні деякі строчки розповім:

  1. import hashlib
  2. import time
  3. from datetime import datetime
  4.  
  5. import settings
  6. from django.shortcuts import redirect
  7. from django.views.generic.simple import direct_to_template
  8. from models import redis
  9.  
  10. def login_require(func):
  11. def wrapper(*args,**kwargs):
  12. session = args[0].COOKIES.get('session','error')
  13. if session != redis.get('admin:session'):
  14. return redirect('/admin/login/')
  15. return func(*args,**kwargs)
  16. return wrapper
  17.  
  18. def login(request):
  19. if request.method == 'POST':
  20. password = request.POST.get('password', '')
  21. if password == str( redis.get('admin:password') ):
  22. response = redirect('/admin/')
  23. rand = str( datetime.now() ) + settings.SECRET_KEY
  24. session = hashlib.sha1(rand).hexdigest()
  25. redis.set('admin:session', session)
  26. response.set_cookie('session', session)
  27. return response
  28. return direct_to_template(request, 'login.html', {})
  29.  
  30. def logout(request):
  31. response = redirect('/')
  32. response.set_cookie('session', '')
  33. return response
  34.  
  35. @login_require
  36. def admin(request):
  37. return direct_to_template(request, 'admin.html', {})
  38.  
  39. @login_require
  40. def add_post(request):
  41. if request.method == 'POST':
  42. id = int(time.time())
  43. redis.push('post:list', id)
  44. redis.set('post:title:%s'%id, request.POST['title'])
  45. redis.set('post:text:%s'%id, request.POST['text'])
  46. redis.set('post:category:%s'%id, request.POST['category'])
  47. redis.push('category:item:%s'%request.POST['category'], id)
  48. for item in request.POST['tags'].split(' '):
  49. redis.push('post:tags:%s'%id, item)
  50. redis.push('tags:item:list:%s'%item, id)
  51. if redis.get('tags:item:count:%s'%item) > 0:
  52. redis.incr('tags:item:count:%s'%item)
  53. else:
  54. redis.set('tags:item:count:%s'%item, 0)
  55. return redirect('/')
  56.  
  57. categories = redis.lrange('category:list', 0, -1)
  58. return direct_to_template(request, 'add_post.html', {
  59. 'categories': categories,
  60. })
  61.  
  62. @login_require
  63. def add_category(request):
  64. if request.method == 'POST':
  65. redis.push('category:list', request.POST['category'])
  66. categories = redis.lrange('category:list', 0, -1)
  67. return direct_to_template(request, 'add_category.html', {
  68. 'categories': categories,
  69. })
  70.  
  71. def main(request):
  72. raw_posts = redis.lrange('post:list', 0, -1)
  73. posts = []
  74. for item in raw_posts:
  75. posts.append({
  76. 'id': item,
  77. 'title': redis.get('post:title:%s'%item),
  78. })
  79.  
  80. return direct_to_template(request, 'main.html', {
  81. 'posts': posts,
  82. })
  83.  
  84. def view(request, id=1):
  85.  
  86. title = redis.get('post:title:%s'%id)
  87. text = redis.get('post:text:%s'%id)
  88. category = redis.get('post:category:%s'%id)
  89. tags = redis.lrange('post:tags:%s'%id, 0, -1)
  90.  
  91. return direct_to_template(request, 'view.html', {
  92. 'title': title,
  93. 'text': text,
  94. 'category': category,
  95. 'tags': tags,
  96. })
  97.  
  98. def tags(request, tags=''):
  99. raw_posts = redis.lrange('tags:item:list:%s'%tags, 0, -1)
  100. posts = []
  101. for item in raw_posts:
  102. posts.append({
  103. 'id': item,
  104. 'title': redis.get('post:title:%s'%item),
  105. })
  106.  
  107. return direct_to_template(request, 'main.html', {
  108. 'posts': posts,
  109. })
  110.  
  111. def category(request, category=''):
  112. raw_posts = redis.lrange('category:item:%s'%category, 0, -1)
  113. posts = []
  114. for item in raw_posts:
  115. posts.append({
  116. 'id': item,
  117. 'title': redis.get('post:title:%s'%item),
  118. })
  119.  
  120. return direct_to_template(request, 'main.html', {
  121. 'posts': posts,
  122. })

В 8-мій строчці йде імпортування моделі, а по суті там створюється обьект для роботи з базою.
Далі з 10-тої строчки йде декоратор для тих views, що відносяться до адмінки. Цей код звіряє сесію, і якщо вона є в базі даних, то пускає в адмінку, якщо ні, то переадресую на форму для вводу пароля.
З 18 строчки функція, що показує поле з паролем, і якщо пароль правильний, то створює сесію в базі даних і записує кукіси юзеру.
З 30 строчки код лише обнуляє сесію.
З 40 строчки код по збереженню постів. Спочатку ми створюємо унікальний ідентифікатор цього посту на основі дати публікації, далі записуємо всі дані в окремі ключі. Також в список з ключем post:list в нас список, де зберігаємо ідентифікатори всіх постів, щоб потім їх змогли знайти. Также бачимо що ми зберігаємо категорію таким кодом в 46 строчкі, а потім в 47 строчці маємо список цієї категорії, і до нього додаємо наш ідентифікатор посту для того щоб потім по категорії змогли знайти всі пости, що в ній. Теж саме ми маємо трохи нижче з тегами, але для тегів ще й зберігаємо кількість постів з цим тегом, для того щоб будувати хмарку тегів.
В принципі далі код вже досить стандартний.
Якщо вас зацікавила база данних Редіс, то вона хоститься тут - http://code.google.com/p/redis/

PS: не потрібно дивитися по безпеці цей код, це лише тестовий приклад. А цікаво ви побачили проблемний участок в безпеці. Це не бага, це завдання на хакерську смикалку ;)

 
 
 
Роман Хоменко aka PresidentUA
mail/jabber: spirt40@gmail.com