Programmer, want to completely understand Redis, this 15:00 you must understand ~ (pure dry)

Tips: content a bit more, there is need to "fast forward" friends, the focus can be viewed according to the directory ~

Redis is an open source, high performance bond - value storage (key-value store). It is often called a server data structure (data structure server).

Redis keys may include a string (strings) type, and further comprising a hash (of the hashes), lists (Lists), set (sets), and an ordered collection (sorted sets) and other data types. For these types of data, you can perform an atomic operation. For example: the string additional operations (the append); hash value is incremented; add elements to the list; calculated intersection of the sets, and sets the difference sets.

In order to obtain superior performance, Redis using the memory (in-memory) data set (DataSet) manner. Meanwhile, Redis support persistent data, you can dump data set from time to time on the disk (snapshot), or append each operation command (append only file, aof) in the tail of the log.

Redis also supports the replication master (master-slave replication), and has a very fast initial synchronization nonblocking (non-blocking first synchronization), automatically reconnect the network disconnection functions. Meanwhile Redis also has a number of other features, including support for simple things, publish-subscribe (pub / sub), pipeline (pipeline) and virtual memory (vm) and so on. Redis has a rich client that supports this stage the most popular programming languages.

This article directory:

Installation Redis
Redis configuration
Redis data types
Redis function
persistent
master-slave replication
transaction support
publish and subscribe
pipeline
virtual memory
Redis performance
Redis deployment
Redis scenarios
Redis summary
Redis install:
Download the latest stable version of Redis ( http://redis.io/download )

tar zxvf redis-2.2.11 decompressor

Programmer, want to completely understand Redis, this 15-point you must understand ~ (pure dry)
cd into the src directory src make the compiler Redismake test can test (this step can be omitted) make install to install, the default installation directory is / usr / local / bin, 5 generates binary files, which can be Kaodao new directory, for example: / usr / local / redis / bin

Programmer, I want to completely understand Redis, this 15:00 you must understand ~ (pure dry)
cp source /src/redis.conf / usr / local / redis / etc configuration files are copied cd /usr/local/redis./ bin / redis-server ./etc/redis.conf start redis Redis service at this time is already running, but to get good performance, but also a reasonable configuration to the configuration file

Redis configuration:

  1. Redis is not a default way daemon is running, you can modify the configuration item, use yes Enable daemon daemonize no
  2. When Redis when running in daemon mode, the default Redis will write /var/run/redis.pid pid file, you can specify pidfile /var/run/redis.pid by pidfile
  3. Redis designated monitor port, the default port is 6379
  4. Host address 127.0.0.1 binding the bind
    5. The when the client closes the connection is idle time, if specified as zero disables this function timeout 300
  5. Specifies the logging level, Redis supports a total of four levels: debug, verbose, notice, warning, the default is verbose loglevel verbose
  6. Logging mode, the default is standard output, if configured to run as a daemon Redis way, but here again configured for logging to standard output, the log will be sent to / dev / null logfile stdout
  7. Set number database, the default database is 0, may be used SELECT <dbid> connected to the specified database command id databases 16
  8. Specifies how long, how many times the refresh operation, the data will be synchronized to the data file, with a plurality of conditions can save <seconds> <changes> Redis default profile provided three conditions: save 900 1 save 300 10 save 60 10000 respectively have a change within 900 seconds (15 minutes) and 300 seconds (5 minutes) 10 10 000 as well as to change the change within 60 seconds.
  9. Huge rdbcompression yes Specifies whether to compress the data stored in the local database, the default is yes, Redis using LZF compression, if the CPU in order to save time, you can turn this option off, but will cause the database files become
  10. Specify the local database file name, the default value dump.rdb dbfilename dump.rdb
  11. Specify the local database storage directory dir ./
  12. When the unit is provided slav service, the service master set of IP address and port, at Redis starts, it will synchronize data automatically from slaveof master <masterip> <masterport>
  13. When the master service is password-protected, slav service connection master password masterauth <master-password>
  14. Provided Redis connection password, if the password is configured connector, when connected to the client password is required by Redis AUTH <password> command, the default closed requirepass foobared
  15. Set the same time the maximum number of client connections, the default maximum number of file descriptors unlimited, Redis can be opened at the same time the number of client connections to Redis process can be opened, if set maxclients 0, an unlimited amount. When the number of clients connected to the limit is reached, Redis closes a new connection back to the client max number of clients reached error maxclients 128
  16. After specifying the maximum memory limit Redis, Redis will be loaded at startup data into memory, the maximum memory, Redis will first try to clear expired or about to expire Key, when this method worked, still reach the maximum memory settings, It will no longer be able to write to, but you can still read operations. Redis vm new mechanism will be stored memory Key, Value will be stored in the swap area maxmemory <bytes>
  17. Specify whether after each update operation logging, Redis default asynchronous data is written to disk, if you do not turn, may result in data over a period of time when power is lost. Because redis itself to synchronize data file is based on the above criteria to save synchronized, so some data will exist only in the memory for some time. The default is no appendonly no
  18. Update log file name is specified, the default is appendonly.aof appendfilename appendonly.aof
  19. Update log specified conditions, there are three possible values: no: represents operating systems such as synchronizing data cache to disk (fast) always: representation after each update manually call fsync () writes data to disk (slow, safe) everysec: represents the synchronization once per second (a compromise, the default value) appendfsync everysec
  20. Specifies whether the virtual memory mechanism is enabled, the default value is no, a brief look, VM data paging mechanism will be stored by the visit of less Redis page that is cold swap data to disk, access to multiple pages automatically swapped out by the disk into memory (in a later article I will carefully analyze the Redis VM mechanism) vm-enabled no
  21. Virtual memory file path, the default value /tmp/redis.swap, not a plurality of instances share Redis vm-swap-file /tmp/redis.swap
  22. All data is greater than vm-max-memory stored in virtual memory, regardless vm-max-memory settings how small, all index data is stored in memory (Redis index data is keys), that is, when vm-max -memory set to 0, the fact is that all value are present in the disk. The default value is 0 vm-max-memory 0
  23. Redis swap file into many of the page, an object can be stored in multiple page above, but on a page can not be shared by multiple objects, vm-page-size according to the size of the data set stored, the authors suggest that if store many small objects, page size is preferably set to 32 or 64bytes; large if large objects stored, you can use a larger Page, if uncertain, the default value vm-page-size 32
  24. Set the number of page swap file, because the page table (one representation page or use the free bitmap) is placed in memory on the disk ,, every eight pages 1byte will consume memory. vm-pages 134217728
  25. Set the number of threads access the swap file, it is best not to exceed the number of core machine, if set to 0, then all operations on the swap file is serial, it may cause relatively long delays. The default value is 4 vm-max-threads 4
  26. Set in the response to the client, whether the smaller packets into one packet is sent, it is enabled by default glueoutputbuf yes
  27. When more than a certain specified amount or the maximum element exceeds a critical value, the hash algorithm uses a special hash-max-zipmap-entries 64 hash-max-zipmap-value 512
  28. Specifies whether to activate the reset hash, is enabled by default activerehashing yes
  29. Designated include other configuration files, you can use the same configuration files between multiple Redis instances on the same host, while each instance has its own specific profile /path/to/local.conf the include
    Redis data types:
    Keys non-binary character type of security (not binary-safe strings)

ValuesStrings Lists Sets Sorted sets Hash

Key: redis nature on a key-value database, we first look at his key first key is a string type, because the key is not binary safe string, so like "my key" and "mykey \ n" so. including spaces and line breaks key is not allowed.

We can define the format of a Key when using your own. For example of the type-Object: the above mentioned id: Field,
Key not too long. Total memory, slow queries.
Key not too short. u: 1000: pwd better to user: 1000: a good password readability

key related commands:

exits key test specified key exists and returns 1 indicating the presence of absence of 0

del key1 key2 .... keyN remove the given key, delete key returns the number of 0 indicates a given key does not exist

type key is returned to the value of the type given key. Or none said they did not exist, key character type has a string, list of list set unordered collection type, etc. ...

keys pattern returns that match the specified pattern of all key (support *,?, [abc] the way), to the following example

Programmer, want to completely understand Redis, this 15-point you must understand ~ (pure dry)
programmer, I want to completely understand Redis, this 15-point you must understand ~ (pure dry)
randomkey return from the current database a randomly selected key, if the current database is empty, empty string is returned rename oldkey heavy atom newkey named a key, newkey if present, will be covered, returns a success, failure 0. Failure does not exist or may be oldkey and newkey renamenx oldkey newkey the same as above, but if there is newkey return dbsize failure to return the current number of key database expire key seconds for the key specified expiration time, in seconds. 1 returns success, 0 key expiration time is already set or the setting of key return key TTL expiration time does not exist a remaining expiration seconds, -1 indicates key does not exist or is not provided through the expiry time select db-index selected by the index database the default is 0 for all database connections, database default number is 16. Returns 1 for success, 0 Failed move key db-index key to move from the current database to the specified database. Return 1 success. 0 if the key does not exist or has been deleted flushdb current database of all key in the specified database, this method will not fail. Flushall all caution to remove all key database, this method will not fail. More caution

String Type:

redis string is the most basic type, and string type is binary safe.

redis The string can contain any data. Including jpg image or serialized object. The maximum limit is 1G byte. If only string type, redis can be seen as a plus memcached persistence characteristics

String Related command:

string set key value is set corresponding to the type of the value key, returns a success, failure 0

setnx key value above, if the key already exists, 0 is returned. nx is not exist mean get key acquisition string value corresponding key, if the key is returned nilgetset key value Set key value does not exist, and returns the old value of the key. If the returned key does not exist nilmget key1 key2 ... keyN obtaining a plurality of values ​​of the key, if the corresponding key is not present, then the corresponding returns nil. The following is an experiment, the absence of nonexisting corresponding returns nil

Programmer, want to completely understand Redis, this 15-point you must understand ~ (pure dry)
mset key1 value1 ... keyN valueN once more set the value of the key, the successful return of 1 means that all values are set, failure return 0 means no value is set msetnx key1 value1 ... keyN valueN above, but does not override existing keyincr key values do Gaga key operation, and returns the new value. Note incr that is not an int value will return an error, incr a nonexistent key, set key to 1decr key above, but to do that and reduction operations, decr a key does not exist, set the key to -1incrby key integer with incr , plus the specified value, key does not exist when it will set the key, and is considered the original value 0decrby key integer with decr, minus the specified value. decrby solely for readability, a negative value we can achieve the same effect by incrby, on the contrary the same.

append key value to the value of the specified key character string added value, returns the length of the new string value. The following give an example

Programmer, want to completely understand Redis, this 15:00 you must understand ~ (pure dry)
substr Start End key interception return off a string of key values, attention does not modify the value of the key. Index is zero-based.

List type:

redis type of list is actually a type of string for each child elements are doubly linked list. We can push, pop operations to add or remove elements from the head end of the list. This makes it both as a stack list, a queue can also be used.

pop operations as well as the list of blocked version. When we [lr] pop the object is a list, if the list is empty, or does not exist, returns nil immediately. But blocking version of b [lr] pop can then be blocked, of course, you can add timeout, also returns nil after a timeout. Why blocked pop version of it, mainly to avoid polling. As a simple example, if we use the list to achieve a work queue. thread can perform tasks call blocking versions of pop to get the task so you can avoid polling to check whether there are tasks exist. When the task to return immediately when the worker thread can also avoid the delay caused by polling.

List of related commands:

lpush key string in the key list corresponding to the head of a string element is added, returns a success, and not the presence of 0 indicates key list type rpush key string supra, is added at the end llen key return key corresponding to the length of the list, return key does not exist 0 , if the key corresponding to the type of error is not returned list lrange key start end return elements within the specified range, the start index from 0, negative values ​​indicate calculated from behind, -1 represents the inverse of the first element, absent key returns an empty list key LTRIM start end interception list, retained within the specified range of elements, successful return 1, key does not exist return an error lset key index value setting target element value list specified, successful return 1, key or the absence of a subscript return an error lrem key count value from key to delete the corresponding list to count the same value elements. Delete all lpop key count is zero when remove elements from the list of head and returns remove elements. If the key corresponding to the list does not exist or is empty returns nil, if the key corresponding to the value is not returned an error list

rpop above, but blpop key1 deleted from the tail ... keyN timeout scan from left to right to return to the first non-empty list were lpop and return, such as blpop list1 list2 list3 0, if the list does not exist, list2, list3 are non for list2 do lpop the air and returns the element removed from the list2. If all of the list is empty or does not exist, blocking timeout seconds, timeout 0 indicates been blocked. When blocked, if there is any client on the key key1 ... keyN performed push operation, the first key on the client is blocked immediately returns. If a timeout occurs, nil is returned. brpop same BLPOP, is removed from a head from a trailing tail deleted rpoplpush srckey destkey from the corresponding list element srckey removed and added to the corresponding list head destkey, and finally returns the element value is removed, the entire operation is atomic If srckey is empty or does not exist returns nil

Set type:

redis set of unordered collection of type string. The maximum set may comprise elements (32 th -12) of elements. set is implemented by a hash table, hash table will be added or deleted automatically as resizing set on the collection of basic type except add delete operation, other useful operations and further comprising a take-set of Assembly (Union), the intersection (intersection ), difference (difference). You can easily implement tag function sns of friends and blog recommended by these operations.

Set related commands:

sadd key member to add a string element, set corresponding to the set of key, returns a success, and returns 0 if the elements in a set, the set key corresponding to the error return srem key member from the key set corresponding to the given element is not present is removed , successful return 1, if the member does not exist or key does not exist returns 0 if not set the type of key value corresponding to an error is returned SPOP key deletion and return key corresponding to an element set in a random, if the set is empty or key in the collection does not exist the same return SPOP nilsrandmember key, a randomly selected element in the set, but does not remove elements smove srckey dstkey member member is removed and added to the corresponding set from the corresponding dstkey srckey set, the entire operation is atomic. A successful return, if the member does not exist in srckey returns 0 if the key is not set return error type SCard key returns the number of elements in the set, if the set key does not exist or is empty return 0sismember key member determines whether the member in the set, the presence of 1,0 represents the return key does not exist or do not exist sinter key1 key2 ... keyN return all the given key intersection sinterstore dstkey key1 ... keyN with sinter, but will also save the intersection to the next dstkey

sunion key1 key2 ... keyN return all the given key and set sunionstore dstkey key1 ... keyN with sunion, and at the same time to save and set sdiff under dstkey key1 key2 ... keyN return all the difference set sdiffstore dstkey given the key the sdiff key1 ... keyN same, and simultaneously sets all elements to save the difference dstkey smembers key set corresponding to the return key, the result is disordered

Sorted Set Type:

And set as sorted set is a collection of elements of type string, except that will be associated with each element of a double type of score. Sorted set is to achieve a mixture of the skip list and hash table. When the elements are added to the collection, a score map element to be added to the hash table, the mapping score another element is added to skip list and ordered according to the score, you can obtain an ordered set of elements .

Sorted Set related commands:

zadd key score member is added to the collection element, the collection element is present in the corresponding score is updated

zrem key member delete the specified element represents a successful return element does not exist if 0zincrby key incr member increases score value corresponding to the member, and then move the element holding skip list ordered. Returns the updated score values ​​zrank key member returns the specified element rank (subscript, non-score) in the collection of elements in the collection is based on score from small to large order of zrevrank key member as above, but elements in the collection is based on score from large to sort small zrange key start end lrange operation similar elements designated section taken from the collection. Returns the ordered results zrevrange key start end supra, the result is returned by the reverse zrangebyscore score key min max score in the return set of elements in a given interval zcount key min max score zcard the return set in the key number of a given return interval set number of elements zscore key element returns the element corresponding to the given scorezremrangebyrank key min max deleted element in the set position in the element zremrangebyscore key min max given collection interval deletion score given interval

Hash type:

redis hash field is a string type and the value of the mapping table. hash particularly suitable for storing objects. Compared to the deposit of each field into a single object type string. An object type stored in the hash will take up less memory, and can be more convenient access to the entire object.

Hash related commands:

hset key field value set hash field to the specified value if the key does not exist, create

hget key field gets the specified hash fieldhmget key filed1 .... fieldN get all of the specified hash filedhmset key filed1 value1 ... filedN valueN also set hash multiple fieldhincrby key field integer

Redis features:
persistence:

redis is a persistent memory database support, that redis often need to synchronize the data in memory to disk to ensure persistence, which is a big advantage is the relatively memcache. redis supports two persistent ways, one is Snapshotting (snapshot) is the default mode, the other is a Append-only file (abbreviation aof) way. Snapshotting snapshot is the default persistent way. In this way the memory data as a snapshot written to the binary file, the default file name is dump.rdb. It can be configured to automatically make a snapshot of persistent manner. We can configure redis in n seconds If more than m key has been modified to automatically make a snapshot, the following is the default snapshot save the configuration save 900 1 # 900 seconds if more than one key is modified, then the initiate snapshots save save 300 10 # 300 seconds content such as more than 10 key is modified, then saved a snapshot launched

Append-only file

aof better than snapshots lasting resistance, due to the use aof persistent way, redis will each write command received through write function to be added to a file (default is appendonly.aof). When redis restart saved by re-executing file write command to rebuild your entire database in memory. Of course, because the os will modify write cache to do in the kernel, it may not be immediately written to disk. Persistence such aof way also still possible to lose some modifications. But we can tell by redis profile we want to force os written to disk through fsync timing function.

There are three ways as follows (default: fsync once per second)

appendonly yes // Enable aof persistent way

appendfsync always // each time it receives a write command to force an immediate write to the disk, the slowest, but to ensure complete persistence, not recommended

appendfsync everysec // once per second compulsory written to disk, performance and persistence has done a good compromise, recommended

appendfsync no // totally dependent os, the best performance, persistence did not guarantee

Master-slave replication:

Master-slave replication allows multiple slave server and a master server has the same copy of the database. Here are some of the features from redis 1.master replication master can have a plurality of slave2. In addition to the plurality of slave connected to the same external master, slave 3 may be connected to another master slave structure formed from FIG master copy will not be blocked. That is when one or more master and slave for initial synchronization data, master client may continue to process incoming requests. Conversely slave during the initial synchronization data will be blocked, can not process client requests. 4. From the master copy can be used to improve the scalability of the system (we can use a plurality of slave dedicated client read request, such as sort processing operations may be used to slave), it may also be used for simple data redundancy. The master can disable data persistence, save only need to comment all master profile configuration, and then only on the persistent configuration data slave.

Programmer, want to completely understand Redis, this 15-point you must understand ~ (pure dry goods)
transactions:

redis support for transactions is still relatively simple. redis can only guarantee a client initiated transaction commands can be executed continuously, but the middle does not insert other client orders. Multi Exec things began to execute a transaction Discard give up things Watch monitor keyUnwatch give up the watch monitor all key command will monitor a given key, when the key exec time if the monitor had changed after calling watch, the entire transaction fails. Note watch the whole key is valid connection, and the same transaction, if disconnected, monitoring and transaction will be automatically removed.

Transaction Demo:

Programmer, want to completely understand Redis, this 15-point you must understand ~ (pure dry)
programmer, want to completely understand Redis, this 15-point you must understand ~ (pure dry)
programmer, you want to completely understand Redis, this 15-point you must understand ~ (pure dry)
programmer, I want to completely understand Redis, this 15-point you must understand ~ (pure dry)

Publish and subscribe:

Publish and subscribe (pub / sub) is a messaging mode of communication. Subscribers can subscribe to type their own messages of interest to redis server through subscribe and psubscribe command, redis message type is called channel (channel). When a publisher sends a particular type of message to redis server through the publish command. All client subscription type of the message will receive this message. Here the message transmission is many to many. A client may subscribe to multiple channel, you can send messages to a plurality of channel. SubscribeUnsubscribePsubscribePunsubscribePublish

Publish and subscribe demo:

A client:

Programmer, want to completely understand Redis, this 15-point you must understand ~ (pure dry)
Client II:

Programmer, want to completely understand Redis, this 15-point you must understand ~ (pure dry)
Client Three:

Programmer, want to completely understand Redis, this 15:00 you must understand ~ (pure dry)

pipeline:

cs mode is a redis tcp server, use and the like http request response protocol. Client can initiate a connection request command through a plurality of socket. Each request command issued after the client usually blocks and waits redis service processing, redis dealt with the request command will result through a response packet back to the client. The basic communication process is as follows Client: INCR

XServer: 1

Client: INCR

XServer: 2

Client: INCR

XServer: 3

Client: INCR

XServer: 4 basically four commands require eight tcp packets to complete. Since the communication to the network latency, if required 0.125 seconds from the time of packet transmission between the client and the server. Then the above four commands eight packets will need at least 1 second to complete.

The pipeline is packaged using multiple commands issued from the client together, without waiting for a single command response is returned, after the end of the service will be processed redis plurality of command sets a plurality of commands to return the processing result packed together to the client. The communication process is as follows

Client: INCR

XClient: INCR

XClient: INCR

XClient: INCR

XServer: 1

Server: 2

Server: 3

Server: 4

Virtual Memory:

redis not use virtual memory mechanism os provided but realized his own virtual memory mechanism, but the ideas and objectives are the same. Is temporarily infrequently accessed data from the memory is swapped to disk, freeing up memory space for other data need to be accessed. Especially for such a redis memory database, memory is always not enough. In addition the data may be divided into a plurality of outer redis server. On another database capacity can be increased vm way is to use the data that is infrequently accessed disk swap. If our data storage is always a small part of the data is accessed frequently, most of the data is rarely accessed, the site is indeed always for only a few users are often active. When a small amount of data to be frequently accessed, using the vm will not only enhance the capacity of a single redis server database, and does not cause too much impact on performance. vm-enabled yes # open the function vm vm-swap-file /tmp/redis.swap # exchange value saved the file path /tmp/redis.swapvm-max-memory 1000000 # maximum memory limit, more than the beginning of the exchange value to disk file vm-page-size 32 # each page size of 32 bytes vm-pages 134217728 # up to how many pages vm-max-threads # 4 number of worker threads for executing an object swapped out value used in the document 0 means no worker threads

Redis performance:
programmers want to completely understand Redis, this 15-point you must understand ~ (pure dry)
Redis Deployment:
programmers want to completely understand Redis, this 15-point you must understand ~ (pure dry)
Redis application scenarios:
1. take the N-date operating data such as typical take your site the latest article, the following way, we can review the latest 5000 collection of ID on the List of Redis, and the part beyond the set get use LPUSH latest.comments <ID> command from the database, insert data into the list after the completion of the collection insert with LTRIM latest.comments 0 5000 command so that it is always only saves the last 5000 ID then we get a page commentary on the client when logic can use the following (pseudocode) FUNCTION get_latest_comments (start, num_items) : id_list = redis.lrange ( "latest.comments", start, start + num_items-1) IF id_list.length <num_items id_list = SQL_DB ( "SELECT ... ORDER BY time LIMIT ... ") END RETURN id_list END If you have a different filter dimensions, such as the latest N pieces of a category, then you can build a Click classification The List, only keep ID, then, Redis is very efficient.

2. Application of charts, operations take TOP N

This is different from the needs of the above requirement that the previous operation time-weighted, this is a condition for the weights, such as per-sort the top, this time we need a sorted set to run for, will you want to sort value is set to score sorted set of the specific data corresponding to the set value, a time required to execute a command ZADD. 3. applications that require precision to set the expiration time of such value you can score above mentioned sorted set the expiration time set to the time stamp, then it can simply sort by expiration time, time to clear stale data, not only is cleared Redis of stale data, you can put in the expiration time as Redis is an index of the data in the database, use Redis to find out what data needs to delete outdated, then precisely delete the records from the database. 4. Application of the counter Redis commands are atomic, you can easily use INCR, DECR counters command to build the system.

5.Uniq operation, acquire certain period of time such that all data re-use of Redis set the value of this data structure is most appropriate, and only need to keep the data to set the throw on the line, meaning a collection set, it will automatically re-ranked. 6. Real-time systems, anti-spam system set by the above mentioned features, you can know whether the end-user a certain operation, you can find a collection of its operation and compared statistics. Not impossible, just think. 7.Pub/Sub to build real-time messaging system Redis Pub / Sub system can build a real-time messaging systems, such as many examples of systems with real-time chat Pub / Sub constructed. 8. Construction system queue list can be constructed using the queue system, constructed using the sorted set may even have priority queue system. 9. The cache Needless to say, performance is better than Memcached (in some respects, is not superior to all), the data structure is more diversified.

Redis summary:
Redis is the best way to use all of the data in-memory. More scenes Redis is used as a replacement for Memcached. When more data types supported in addition to key / value using Redis more appropriate. When the stored data can not be removed, the use of more appropriate Redis. (Persistence)

And efficient access to data stored on the mass data concurrent read and write data to the high scalability and high availability (distributed)

What problems or have any different idea, welcome message communication

Special Note: This article material from the network, only to share learning, if infringement, please contact deleted!

Recommended Reading

Redis 2020. entry to the master full set of video tutorials (pure dry goods Series)

Guess you like

Origin blog.51cto.com/14751386/2480847