Cảm ơn bạn đã đọc và ủng hộ blog KTMT ʘ‿ʘ Từ bây giờ chúng tôi sẽ là kipalog.com !

Comments

1. Giới thiệu

Trong bài viết tìm hiểu redis phần 1, chúng ta đã tìm hiểu cách redis quản lý dữ liệu (AOF và RDB) cũng như cách redis tận dụng các tính năng của OS (fsync) để sao lưu dữ liệu. Bài viết này tập trung trình bày cụ thể hơn về framework lập trình hướng sự kiện của redis.

2. Lập trình hướng sự kiện

Lập trình hướng sự kiện không phải là khái niệm mới, mà là một paradigm dược sử dụng từ rất lâu. Trong lập trình GUI (Giao diện đồ họa), khi người dùng click hay di chuyển chuột, các framework đồ họa thường hỗ trợ các phương pháp như onClick, onMouseMove … cho phép người dùng định nghĩa hành vi của hệ thống cho những sự kiện đấy.

Các hệ thống Unix(BSD, MacOS)/Linux/Solaris từ lâu đã hỗ trợ lập trình hướng sự kiện. Mỗi hệ điều hành cung cấp API cho phép lập trình viên chỉ định 1 tập các file descriptor hoặc mốc thời gian (time-event) cần theo dõi và sẽ trigger mỗi sự kiện khi các file descriptor thay đổi trạng thái (có đọc hoặc ghi) hoặc khi một mốc thời gian quan trọng đã đến. Lập trình viên hệ thống chỉ cần cung cấp 1 hàm callback và các API này sẽ thực hiện chạy các callback này. Cụ thể:

  • select Chuẩn POSIX đình nghĩa hàm này.
  • Unix (BSD, MacOS): kqueue
  • Linux: poll, epoll (edge-trigger)
  • Solaris: event ports (port_associate)

Việc các hệ thống đều hỗ trơ cơ chế event multiplexing là điều tốt (hệ thống của bạn sẽ không phải thay đổi design nếu muốn hỗ trợ 1 hệ thống đặc biệt) tuy vậy có một khó khăn đó là: các API này có interface khác nhau. Do vậy đoạn code dùng epoll sẽ không thể nào chạy trên các Unix based và ngược lại một đoạn code dùng kqueue sẽ không chạy được trên linux. Để giải quyết vấn đề này, redis cung cấp 1 layer hướng sự kiện và thay đổi backend API (kqueue, event ports, epoll) theo hệ thống mà redis được biên dịch trên đó.

3. Framework

a. Kiến trúc

                    ╒========================╕
                    |  Redis layer cao hơn   | (aof, rdb, cron...)
                    ╘========================╛

                    ╒========================╕
                    |     API layer hướng    | frontend: aeCreateEventLoop, aeStop, aeCreateFileEvent... 
                    |       sự kiện          | backend: aeAddEvent, aeDelEvent, aeApiPoll...
                    ╘========================╛

    ╒=============╕     ╒=============╕     ╒=============╕     ╒=============╕
    |   select    |     |   kqueue    |     |   epoll     |     | event ports |
    ╘=============╛     ╘=============╛     ╘=============╛     ╘=============╛
    ae_select.c     ae_kqueue.c             ae_epoll.c               ae_evport.c

Để hỗ trợ các event multiplexing api khác nhau của các hệ điều hành, redis xây dựng 1 api layer đứng giữa các layer cao hơn và các api của OS (như trong hình vẽ). Layer này có 2 loại api khác nhau: frontend và backend.

  • frontend api: là các api cho phép các layer ở trên thao tác với các sự kiện và vòng lặp sự kiện (event loop). Các api này gồm có: aeCreateEventLLoop, aeStop, aeMain, aeCreateFileEvent, aeCreateTimeEvent…
  • backend api: thực chất là interface api. Các api hệ thống khác nhau sẽ được viết để phù hợp với interface này. Interface này gồm các api như: aeAddEvent, aeDelEvent, aeApiPoll, aeApiName.

frontend api sẽ gọi backend api để taoji các sự kiện, poll các file descriptor… Các backend api sẽ đối chiếu sử dụng tương ứng với api của hệ thống. Việc sử dụng api nào sẽ được quyết định lúc biên dịch.

Để thống nhất các polling api khác nhau về cùng 1 interface, redis định nghĩa các cấu trúc dữ liệu giống nhau với mỗi api, cụ thể là các sự kiện và vòng lặp sự kiện. Các cấu trúc này được viết trong file ae.h

ae.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#ifndef __AE_H__
#define __AE_H__

#define AE_OK 0
#define AE_ERR -1

#define AE_NONE 0
#define AE_READABLE 1
#define AE_WRITABLE 2

#define AE_FILE_EVENTS 1
#define AE_TIME_EVENTS 2
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
#define AE_DONT_WAIT 4

#define AE_NOMORE -1

/* Macros */
#define AE_NOTUSED(V) ((void) V)

struct aeEventLoop;

/* Types and data structures */
typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);
typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData);
typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData);
typedef void aeBeforeSleepProc(struct aeEventLoop *eventLoop);

/* File event structure */
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;

/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc;
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *next;
} aeTimeEvent;

/* A fired event */
typedef struct aeFiredEvent {
    int fd;
    int mask;
} aeFiredEvent;

/* State of an event based program */
typedef struct aeEventLoop {
    int maxfd;   /* highest file descriptor currently registered */
    int setsize; /* max number of file descriptors tracked */
    long long timeEventNextId;
    time_t lastTime;     /* Used to detect system clock skew */
    aeFileEvent *events; /* Registered events */
    aeFiredEvent *fired; /* Fired events */
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* This is used for polling API specific data */
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;

/* Prototypes */
aeEventLoop *aeCreateEventLoop(int setsize);
void aeDeleteEventLoop(aeEventLoop *eventLoop);
void aeStop(aeEventLoop *eventLoop);
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData);
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask);
int aeGetFileEvents(aeEventLoop *eventLoop, int fd);
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc);
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id);
int aeProcessEvents(aeEventLoop *eventLoop, int flags);
int aeWait(int fd, int mask, long long milliseconds);
void aeMain(aeEventLoop *eventLoop);
char *aeGetApiName(void);
void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep);

#endif

Theo như ae.h, redis có 3 kiểu sự kiện khác nhau:

  • Sự kiện trên File (đọc, ghi) (aeFileEvent)
  • Sự kiện thời gian (aeTimeEvent)
  • Sự kiện đã được triggered (aeFiredEvent)

Các callback prototype đều nhận đối số đầu tiên là 1 con trỏ chỉ đến cấu trúc aeEventLoop. Cấu trúc này quản lý rất nhiều thông tin khác nhau như: số sự kiện được đăng ký, sự kiện được gọi, file descriptor lớn nhất đang quản lý, danh sách các sự kiện thời gian v.v

b. Chi tiết thực hiện

Toàn bộ quá trình xử lý sự kiện được bắt đầu bằng cách gọi aeMain. Xử lý trong aeMain thực chất là một vòng lặp gọi hàm xử lý sự kiện: aeProcessEvents. Tất cả các sự kiện sẽ được thực thi ở hàm aeProcessEvents này. Ta hãy cùng tìm hiểu công việc mà aeProcessEvent phải làm.

ae.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

/* Process every pending time event, then every pending file event
 * (that may be registered by time event callbacks just processed).
 * Without special flags the function sleeps until some file event
 * fires, or when the next time event occurs (if any).
 *
 * If flags is 0, the function does nothing and returns.
 * if flags has AE_ALL_EVENTS set, all the kind of events are processed.
 * if flags has AE_FILE_EVENTS set, file events are processed.
 * if flags has AE_TIME_EVENTS set, time events are processed.
 * if flags has AE_DONT_WAIT set the function returns ASAP until all
 * the events that's possible to process without to wait are processed.
 *
 * The function returns the number of events processed. */
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;

        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
        if (shortest) {
            long now_sec, now_ms;

            /* Calculate the time missing for the nearest
             * timer to fire. */
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
            tvp->tv_sec = shortest->when_sec - now_sec;
            if (shortest->when_ms < now_ms) {
                tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
                tvp->tv_sec --;
            } else {
                tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
            }
            if (tvp->tv_sec < 0) tvp->tv_sec = 0;
            if (tvp->tv_usec < 0) tvp->tv_usec = 0;
        } else {
            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to set the timeout
             * to zero */
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                /* Otherwise we can block */
                tvp = NULL; /* wait forever */
            }
        }

        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

      /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            if (fe->mask & mask & AE_READABLE) {
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }
            processed++;
        }
    }
    /* Check time events */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}

Hàm aeProcessEvents làm 3 nhiệm vụ chính:

  • Tìm sự kiện có thời gian timeout gần nhất
  • Lắng nghe sự kiện File, với thời gian poll không quá thời gian sự kiện gần nhất ở trên.
  • Với các sự kiện được triggered (file có thể đọc, ghi; thời gian timeout đã đến), chạy các callback được đăng ký với các sự kiện.

Việc tìm thời gian timeout gần nhất ở đầu vòng lặp nhằm hạn chế thấp nhất khả năng delay của các sự kiện thời gian (Nên nhớ serverCron chạy với thời gian timeout 1ms trên 1 lần).

Sau khi xử lý lần lượt các xử lý các sự kiện file, redis sẽ xử lý các sự kiện thời gian. Xử lý sự kiện thời gian cũng khá đơn giản. Redis lần lượt xét từng sự kiện thời gian trong danh sách các sự kiện thời gian và gọi callback với các sự kiện đã quá thời hạn. Tuy nhiên, ta sẽ thấy 1 đoạn code khá mập mờ ở đầu xử lý sự kiện thời gian, với comment như dưới đây:

ae.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /* If the system clock is moved to the future, and then set back to the
     * right value, time events may be delayed in a random way. Often this
     * means that scheduled operations will not be performed soon enough.
     *
     * Here we try to detect system clock skews, and force all the time
     * events to be processed ASAP when this happens: the idea is that
     * processing events earlier is less dangerous than delaying them
     * indefinitely, and practice suggests it is. */
    if (now < eventLoop->lastTime) {
        te = eventLoop->timeEventHead;
        while(te) {
            te->when_sec = 0;
            te = te->next;
        }
    }
    eventLoop->lastTime = now;
    ...

Làm sao thời gian hệ thống trả về bởi time(NULL) có thể nhỏ hơn thời gian xử lý được ghi nhận lần trước đấy được? Thực chất ở đây, antirez đã cân nhắc rất kỹ 1 tính huống có thể xảy ra với hệ thống thời gian của Linux. Trong điều kiện hoạt động bình thường, thời gian hệ thống sẽ luôn tăng. Tuy vậy, với 1 số trường hợp rủi ro:

  • Nguồn cung cấp điện không đủ.
  • Pin CMOS có vấn đề.

1s trong máy tính có thể bằng 2, 3s trong thời gian thực, nói cách khác đồng hồ máy tính sẽ bị chạy chậm đi. Với tình huống này các sự kiện thời gian sẽ bị sai lệch và redis sẽ hoạt động không bình thường. Đấy chính là lý do antirez thêm đoạn code trên.

4. Redis dùng framework này như thế nào?

  • Sự kiện file được sử dụng ở redis client và cluster. Thực chất các redis instance cần phải liên lạc với nhau để trao đổi dữ liệu. Việc trao đổi này tiến hành qua mạng và vì vậy hệ thống không thể nào biết khi nào dữ liệu mới sẽ đến. Thay vì phải chờ dữ liệu, bằng cách dùng framework sự kiện, hệ thống có thể tiến hành các xử lý có ưu tiên cao hơn môt cách bất đồng bộ, nâng cao hiệu năng của hệ thống.
  • Sự kiện thời gian được sử dụng để định kỳ gọi cronServer (Nhiệm vụ của cronServer: tìm hiểu redis phần 1).

5. Kết luận

Redis sử dụng phương pháp lập trình hướng sự kiện để định kỳ gọi các thủ tục backup dữ liệu cũng như quản lý các kết nối từ client. Redis hỗ trợ kqueue, epoll và event port nên hiệu năng đạt được khá cao.

6. Tham khảo

  1. C10K
  2. kqueue
  3. epoll
  4. event ports
  5. redis mailing list
  6. IOCP Input/output completion port
Comments

Preface

Trước khi đọc bài này, tôi có 1 vài recommend cho độc giả :)

  1. Bạn nên đọc trước bài viết về Builder Pattern trong Java cũng trong blog ktmt, sẽ có 1 cái nhìn tổng quát và hình dung dễ dàng hơn về ứng dụng của các pattern trong programming.

  2. Có hàng tá bài viết về Inversion Of Control và Dependency Injection. Try to google it first.

  3. Nếu không, nhớ google thêm sau khi đọc bài viết :D

Dependency Injection

Chúng ta sẽ bắt đầu với 1 ví dụ gần giống ví dụ trong bài viết về Builder Pattern ở trên. Xem đoạn code sau. Ngôn ngữ ở đây là PHP.

Book.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Book ()
{
    public function __construct()
    {
        $this->title = new Title;
        $this->author = new Author;
        $this->genre = new Genre;
        $this->publishDate = new PublishDate;
        $this->ISBN = new ISBN;
    }
}
...

$book = new Book;
?>

Ở đây giả sử Title, Author, Genre, PublishDate hay ISBN đều là các class đã được định nghĩa trước. Như vậy class Book có 5 dependency là 5 class kể trên.

Về mặt technical, chẳng có gì là không ổn với 1 class như trên cả. Tuy nhiên programmer có kinh nghiệm sẽ dễ dàng nhận thấy chúng ta đã hardcoded 5 dependency trên vào trong Book. Nói cách khác nếu muốn Book chứa những dependency khác, chẳng có cách nào khác là sửa lại định nghĩa class.

Như vậy, để tránh những phiền phức nói trên và tạo độ linh hoạt khi sử dụng, class Book nên được viết lại như sau:

Book.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Book ()
{
    public function __construct($title, $author, $genre, $publishdate, $isbn)
    {
        $this->title = $title;
        $this->author = $author;
        $this->genre = $genre;
        $this->publishDate = $publishdate;
        $this->ISBN = $isbn;
    }
}
...

$book = new Book (new Title, new Author, new Genre, new PublishDate, new ISBN)

?>

Bạn có thể thấy, ý tưởng của Dependency Injection(DI) thực ra rất đơn giản, chỉ là bạn vẫn thường sử dụng và không để ý. Dependency có thể được inject theo nhiều kiểu, ví dụ bên trên là constructor injection. Chúng ta còn có setter injection như sau:

Book.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class Book ()
{
    public function __construct()
    {
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

...
// Here we have 4 more methods : setAuthor ,setGenre, setPublishDate, setISBN
}
...

$book = new Book;
$book->setTitle(new Title);
$book->setAuthor(new Author);
$book->setGenre(new Genre);
$book->setPublishDate(new PublishDate);
$book->setISBN(new ISBN);

?>

Và vấn đề mới lại nảy sinh! Có quá nhiều setter và điều đó biến Book thành 1 class phức tạp khi sử dụng. Việc viết lại tất cả các setter khi khởi tạo 1 Book thật là painful !

Để giải quyết vấn đề kể trên, chúng ta sẽ đến với design pattern tiếp theo: Inversion of Control (IoC)

Inversion of Control

In software engineering, inversion of control (IoC) is a programming technique, expressed here in terms of object-oriented programming, in which object coupling is bound at run time by an assembler object and is typically not known at compile time using static analysis.

Giải thích lý thuyết về IoC có lẽ sẽ tốn nhiều công sức, như recommend trên đầu bài, bạn có thể google 1 chút về IoC. Ở đây tôi sẽ đưa ra luôn 1 implement để sử dụng với class Book kể trên.

IoC.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
class IoC {
   protected static $registry = array();

   // Register
   public static function register($name, Closure $resolve)
   {
      static::$registry[$name] = $resolve;
   }

   // Resolve
   public static function resolve($name)
   {
      if ( static::registered($name) )
      {
         $name = static::$registry[$name];
         return $name();
      }

      throw new Exception('Nothing registered with that name, fool.');
   }

   // Check resigtered or not
   public static function registered($name)
   {
      return array_key_exists($name, static::$registry);
   }

}
?>

WTH! Cái khỉ gì trông lằng nhằng quá phải không :D

Đừng lo lắng, để hiểu đoạn code trên trước hết hãy để ý rằng ở đây chúng ta có rất nhiều các static function. Static function có thể gọi trục tiếp trên class chứ không phải trên instance thông qua cách gọi “Class::StaticMethod()”. Ngoài ra Closure là 1 anonymous function. Bạn sẽ hiểu ngay khi xem cách dùng dưới đây

Book.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
IoC::register('book', function(){
    $book = new Book;
    $book->setTitle(new Title);
    $book->setAuthor(new Author);
    $book->setGenre(new Genre);
    $book->setPublishDate(new PublishDate);
    $book->setISBN(new ISBN);

    return $book;
});
...

$book = IoC::resolve('book');

?>

Woo! Bây giở mỗi khi muốn tạo 1 instance của Book với đầy đủ các dependency, chỉ cần IoC::resolve('book'). Cùng với đó, các dependency có thể inject thông qua IoC::register('book',function(){...}). Đến khi unit test, bạn có thể dùng IoC::register để mocking các dependency và test Book mà không khởi tạo Title,Author…

Singleton pattern with IoC

Bạn thử tưởng tượng, nếu như phần register ‘book’ bên trên chiếm nhiều tài nguyên, có thể bạn sẽ không muốn mỗi lần resolve lại khởi tạo 1 instance mới. Nói cách khác, bạn chỉ muốn chỉ có 1 Book với đầy đủ Title, Author, … được khởi tạo 1 lần, và lần sau muốn sử dụng thì gọi lại chính instance đã được tạo.

Đây là đất diễn của Singleton design pattern :) Tôi sẽ thêm static function singleton cho IoC như sau:

IoC.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
class IoC {
  protected static $registry = array();
  protected static $shared = array();

  // Register, here save the Closure to static::$registry
  public static function register($name, Closure $resolve)
  {
     static::$registry[$name] = $resolve;
  }

  // Singleton, Note that here we save the result of Closure, not the Closure
  public static function singleton($name, Closure $resolve)
  {
    static::$shared[$name] = $resolve();
  }

  // Resolve, consider register or singleton here
  public static function resolve($name)
  {
    if ( static::registered($name) )
    {
      $name = static::$registry[$name];
      return $name();
    }

    if ( static::singletoned($name) )
    {
      $instance = static::$shared[$name];
      return $instance;
    }

    throw new Exception('Nothing registered with that name, fool.');
  }

  // Check resigtered or not
  public static function registered($name)
  {
     return array_key_exists($name, static::$registry);
  }


  // Check singleton object or not
  public static function singletoned($name)
  {
    return array_key_exists($name, static::$shared);
  }

}
?>

Và bây giờ

Book.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
IoC::singleton('book', function(){
    $book = new Book;
    $book->setTitle(new Title);
    $book->setAuthor(new Author);
    $book->setGenre(new Genre);
    $book->setPublishDate(new PublishDate);
    $book->setISBN(new ISBN);

    return $book;
});
...

$book1 = IoC::resolve('book');
$book2 = IoC::resolve('book'); // exactly same instance with $book1

?>

Bạn có thể lấy đoạn code sample trên Gist về chạy thử. Have fun with IoC :)

Real-World Use Case

Đọc đến đây có thể bạn sẽ hỏi tôi, việc quái gì phải xoắn cái IoC này thế, nó có thực sự hữu dụng hay chỉ là 1 cái pattern mang tính demo biếu diễn ?

Chúng ta hãy cùng ghé qua Laravel, 1 framework hiện đại của PHP.

Ở Laravel, IoC đã được chuẩn bị sẵn và không chỉ dùng 1 mình, còn kết hợp với ServiceProvidersFacades để tăng tối đa độ linh hoạt của code base. Một Facades (lại là 1 design pattern khác - hãy google sau khi đọc bài này :) ) có thể được kết nối với IoC và UnitTest như sau :

Facades.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

// binding IoC
App::bind('book', function()
{
    return new Book;
});

// binding facades to IoC
use Illuminate\Support\Facades\Facade;

class FacadesBook extends Facade {

    protected static function getFacadeAccessor() { return 'book'; }
}

Tại sao đã bind class Book vào IoC book rồi, lại còn tiếp tục bind IoC book và Facades FacadesBook lần nữa?

Facades trong Laravel có thể “biến thành” Mock object sau khi gọi method shouldReceive (a magic method :D)

Facades.php
1
2
3
4
5
6
<?php
//Use Book as usual:
$book = FacadesBook::AnInstanceMethodOfBookClass($AParams);
//Mocking for UnitTest:
FacadesBook::shouldReceive('AnInstanceMethodOfBookClass')->once()->with($AParams)->andReturn($FakeValue);
$mockBook = FacadesBook::AnInstanceMethodOfBookClass($AParams);

$book sẽ trả về giá trị thực khi thực hiện method AnInstanceMethodOfBookClass của class Book, trong khi đó $mockBook sẽ trả về $FakeValue.

Summary

  • Dependency Injection: Đưa các dependency vào class thông qua constructor hoặc setter, không khỏi tạo trực tiếp bên trong class.
  • Inversion of Control: bind object vào thời điểm run time, không phải vào thời điểm compile time.
  • Singleton: Design pattern, cho phép trong 1 hệ thống chỉ có 1 instance duy nhất của class được tồn tại.
Comments

Mở đầu

Chắc hẳn các bạn đã biết về các dịch vụ rút gọn url, điển hình là bit.ly. Mục đích của dịch vụ này là nhằm thu gọn là những url rất dài để tiết kiệm chữ (cho những dịch vụ giới hạn về số kí tự như twitter chẳng hạn) và để cho url nhìn gọn hơn. Cơ chế của một dịch vụ rút gọn url khá đơn giản, vậy tại sao không tự làm một dịch vụ cho chính mình. Bài này mình sẽ hướng dẫn cách làm một url shorten service đơn giản dựa trên sinatra và redis.

Cài đặt

Để cài đặt sinatra thì bạn phải có ruby đã cài sẵn trên máy, việc cài đặt sinatra khá đơn giản thông qua gem:

sinatra_install.sh
1
  gem install sinatra

Tiếp đến là redis, để cài đặt redis thì tùy thuộc vào hệ điều hành, trên mac-osx các bạn có thể cài đặt rất dễ dàng thông qua brew:

redis_install.sh
1
  brew install redis

Trên linux hoặc windows các bạn có thể google để tìm ra hướng dẫn cài tương ứng. Các bạn khởi động redis thông qua, khi khởi động redis mà không set option với config gì cả thì redis sẽ chạy trên localhost và port là 6789:

redis_install.sh
1
  redis-server

Giới thiệu qua về sinatra và redis

Sinatra là một mini webframework based trên Rack(Rack là web server interface rút gọn nhất có thể rất nổi tiếng trên ruby). Sinatra cung cấp cho bạn một DSL(Domain specific language) để có thể build một web app một cách dễ dàng nhất. Các bạn có thể tìm hiểu về sinatra ở trang chủ của sinatra.

Redis là hệ thống lưu trữ key-value với rất nhiều tính năng và được sử dụng rộng rãi. KTMT blog đã có một bài viết về redis cách đây không lâu, các bạn có thể tham khảo lại. Về cách sử dụng redis, các bạn có thể tham khảo tại trang chủ redis

Thiết kế chương trình

Cơ chế của một dịch vụ thu gọn hết sức đơn giản, được thể hiện ở diagram dưới đây:

Chắc bạn nào đã dùng bit.ly sẽ tưởng tượng ra usage flow một dịch vụ rút gọn url nên có. Cơ chế chúng ta dùng ở bài viết này như sau: đầu tiên user sẽ gửi url cần rút gọn thông qua form input. Web server nhận được request này sẽ hash url lại thành một chuỗi ngắn hơn, thông thường từ 5~10 kí tự, webserver sẽ lưu lại cặp vào redis, và trả lại chuỗi hash đó được ghép vào url của dịch vụ : http://your-service.com/hash.

Khi user click vào http://your-service.com/hash, đầu tiên server sẽ dùng chuỗi hash tìm original url trên redis, sau đó sẽ trả về response 301 (redirect) với location mới là original url, nhờ vậy mà user sẽ được redirect đến original url.

Như vậy là đã định hình ý tưởng nên làm gì, chúng ta sẽ bắt tay vào code

Coding

  1. Tạo khung Đầu tiên là sườn của một Sinatra app, có get, post. Chúng ta tạo folder cho app của chúng ta và tạo 1 file là app.rb chính là sinatra app:
make.sh
1
2
mkdir url_shorten && cd url_shorten
touch app.rb && vim app.rb

Sau đó chúng ta sẽ tạo cái khung cho sinatra app như dưới đây:

url_shorten.rb
1
2
3
4
5
6
7
8
9
10
11
12
require 'sinatra/base'
require 'redis'

class UrlShort < Sinatra::Base
  set :public_dir, File.dirname(__FILE__) + '/static'

  get '/' do
    erb :index
  end
end

UrlShort.run!

Sinatra có syntax dạng DSL: get ‘some info’ do ‘something’ để represent cho ‘get’ request rất dễ hiểu. Đoạn code trên có nghĩa là khi request đến ‘/’ (root) thì sẽ render file index nằm trong views. Như vậy chúng ta đã có cái khung đơn giản nhất của một web service, nhận request, trả về view. (erb là template engine có sẵn của ruby, nó sẽ render file có đuôi erb ra html)

Tiếp đến chúng ta sẽ thực hiện xử lý hash url. Về mặt lý thuyết thì gọi là hash không đúng lắm vì hash phải là nhận đầu vào X và trả lại Y là kết quả của việc hash X, bài toán của chúng ta ở đây chỉ đơn thuần là generate ra một chuỗi random để represent cho một cái url, như vậy bài toán của chúng ta sẽ là một hàm random_hash(N) nhận đầu vào N là độ dài của chuỗi input và đầu ra là một chuỗi random (cả chữ cả số) có độ dài N, ví dụ như sau:

hash.rb
1
  rand_hash(5) #=> "s4xA6"

Để giải quyết bài toán này thì có khá nhiều hướng đi:

  1. Loop từ 1 đến 5 rồi với mỗi lần loop bạn random ra một số hoặc chữ cái.
  2. Tạo ra 1 array có độ dài 64 gồm từ [0..9] [a..z] và [A..Z] và random ra 5 vị trí trong đó (việc này có thể thực hiện rất dễ dàng thông qua Array#sample của ruby).
  3. Sử dụng base64. base64 có một đặc điểm là sẽ biến 1 số M thành 1 chuỗi cả chữ cả số có độ dài max là log(64)(M). Do đó để tạo ra một chuỗi random có length N thì chúng ta chỉ cần random một số M nằm trong khoảng 64^(N-1) đến 64^(N) và chuyển nó về base 64.
  4. Sử dụng một số kĩ thuật generate 64bit (mà đã được giới thiệu ở bài viết về generate 64bit uid trên KTMT gần đây.

Ở bài viết này chúng ta sẽ sử dụng kĩ thuật số (3). Chúng ta đưa hàm generate hash vào trong helper của sinatra thông qua hàm helpers như sau:

hash.rb
1
2
3
4
5
6
7
8
9
class UrlShort < Sinatra::Base
  ..
  helpers do
    def rand_hash(length)
      gap = 64**(length) - 64**(length-1)
      (rand(gap) + 64**(length-1)).to_s(64)
    end
  end
  ...

Tiếp theo chúng ta sẽ làm nhiệm vụ gắn hash thu được với url thành một cặp key-value và ghi vào redis. Để làm nhiệm vụ này thì đầu tiên chúng ta cần khởi tạo redis, mình khởi tạo redis bằng cách overwrite constructor của app và đưa redis instance vào thành 1 instance property. Ngoài ra, chúng ta cũng không muốn 1 url mà mỗi lần request lại tạo một hash khác nhau , rất tốn tài nguyên, do đó mỗi lần gen hash mình sẽ lưu duplicate thành 2 cặp key-value. Một cặp chứa url làm key và hash làm value, và một cặp chứ hash làm key và url là value, điều này đảm bảo mối liên hệ giữa url/hash là 1/1.

implement.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
post '/' do
  @error = nil
  @error = 'please enter url' if URI.regexp.match(params[:url]).nil?
  @success = false

  unless @error
    if params[:url] and not params[:url].empty?
      @url = params[:url]
      @hash = rand_hash(5)
      exist = @redis.setnx "url:#{@url}", @hash
      if exist #key not set
        @redis.setnx "hash:#{@hash}", @url
      else
        @hash = @redis.get "url:#{@url}"
      end
      @success = true
    end
  end

  erb :index
end

Như vậy chúng ta đã có @hash để trả về cho user, chúng ta sẽ ghep hash vào trong views để hiển thị cho user (views/index.rb)

index.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<form id="form" method="post">
  <input type="text" value="" name="url" id="url"/>
  <input type="submit" value="shorten" id="submit" class="submit"/>
</form>
<hr/>
<div class="mes">Result</div>
<% if @error %>
  <div id='error'>
    <%= @error %>
  </div>
<% else %>
  <% if @success %>
    <a id="result" href='<%= "#{escape_html(url)}#{@hash}" %>'><%= "#{escape_html(url)}#{@hash}" %></a>
    <input data-clipboard-text='<%="#{escape_html(url)}#{@hash}" %>' type="button" id="yank" class="submit" value="yank"/>
  <% end %>  
<% end %>

Phần việc còn lại chúng ta phải giải quyết là khi user click vào link. Link của chúng ta có dạng là www.my-application.com/#{hash}, với sinatra để lọc phần hash hết sức đon giản vì sinatra đã tự động lọc hộ chúng ta và đưa vào biến global params, do đó chúng ta chỉ cần lấy hash từ params, tìm trong redis, và redirect user là ok:

redirect.rb
1
2
3
4
  get '/:hash' do
    url = @redis.get "hash:#{params[:hash]}"
    redirect url
  end

Như vậy chúng ta đã có một flow hoàn chỉnh rồi, thêm tí css và sử dụng ZeroClipboard để có nút yank để copy url vào clipboard, chúng ta đã có một dịch vụ rút gọn url cho riêng mình!

Toàn bộ source code cho tutorial này mình đang để ở đây, mọi người có thể sử dụng tùy ý :). https://github.com/ktmt/link_shorttener

Copyright © 2015 kỹ thuật máy tính