|
| 1 | ++++ |
| 2 | +date = '2025-08-23T8:00:00+08:00' |
| 3 | +draft = false |
| 4 | +title = 'Redis List' |
| 5 | +tags = ['Redis'] |
| 6 | ++++ |
| 7 | + |
| 8 | +## List 列表 |
| 9 | +Redis 的列表是一种线性的有序结构, 可以按照元素被推入列表的顺序来存储元素, 这些元素即可以是文字顺序, 也可以是二进制顺序, 且元素可重复出现. |
| 10 | + |
| 11 | +- LPUSH: 将元素推入列表左端 |
| 12 | + ``` |
| 13 | + LPUSH list item [item item ...] |
| 14 | + ``` |
| 15 | + LPUSH 命令会返回当前元素数量 |
| 16 | +
|
| 17 | +- RPUSH: 将元素推入列表右端 |
| 18 | + ``` |
| 19 | + RPUSH list item [item item ...] |
| 20 | + ``` |
| 21 | +
|
| 22 | +- LPUSHX, RPUSHX: 只对已存在的列表执行推入操作 |
| 23 | + 上面两条命令, 在列表不存在的情况下, 会自动创建空列表, 并将元素推入列表中. |
| 24 | + 且上面命令每次只能推入一个元素 |
| 25 | +
|
| 26 | +- LPOP: 弹出列表最左端的元素, 并返回被移出的元素 |
| 27 | + ``` |
| 28 | + POP list |
| 29 | + ``` |
| 30 | + 空列表 POP 会返回空值 (nil) |
| 31 | +
|
| 32 | +- RPOP: 弹出列表最右端的元素 |
| 33 | + ``` |
| 34 | + RPOP list |
| 35 | + ``` |
| 36 | +
|
| 37 | +- RPOPLPUSH: 将列表右端弹出的元素推入列表左端 |
| 38 | + ``` |
| 39 | + RPOPLPUSH source target |
| 40 | + ``` |
| 41 | + source 和 target 可以是相同列表, 也可以是不同列表. 但不能为空列表, 否则会返回空(nil) |
| 42 | +
|
| 43 | +### 示例: 先入先出队列 |
| 44 | +许多电商网站都会在节日时推出一些秒杀活动, 这些活动会放出数量有限的商品供用户抢购, 秒杀系统的一个特点就是短时间内会有大量用户进行相同的购买操作, 如果使用事务或者锁去实现秒杀程序, 那么会因为锁和事务的重试性而导致性能低下, 并且由于重试的存在, 成功购买商品的用户可能并不是最早购买操作的用户, 因此这种秒杀系统并不公平. |
| 45 | +解决方法之一就是把用户的购买操作都放入先进先出队列里面, 然后以队列的方式处理用户购买操作, 这样的程序就可以不使用锁或者事务实现秒杀系统, 且更加公平. |
| 46 | +```Python |
| 47 | +from redis import Redis |
| 48 | +
|
| 49 | +class FIFOqueue: |
| 50 | + def __init__(self, client, key): |
| 51 | + self.client = client |
| 52 | + self.key = key |
| 53 | + def enqueue(self, item): |
| 54 | + return self.client.rpush(self.key, item) |
| 55 | + def dequque(self): |
| 56 | + return self.client.lpop(self.key) |
| 57 | +
|
| 58 | +client = Redis(decode_responses=True) |
| 59 | +q = FIFOqueue(client, key="buy-request") |
| 60 | +
|
| 61 | +print("Enqueue:", q.enqueue("peter-buy-milk"), "peter-buy-milk") |
| 62 | +print("Enqueue:", q.enqueue("john-buy-rice"), "john-buy-rice") |
| 63 | +print("Enqueue:", q.enqueue("david-buy-keyboard"), "david-buy-keyboard") |
| 64 | +
|
| 65 | +print("Dequeue:", q.dequque()) |
| 66 | +print("Dequeue:", q.dequque()) |
| 67 | +print("Dequeeu:", q.dequque()) |
| 68 | +``` |
| 69 | + |
| 70 | +- LLEN: 获取列表长度 |
| 71 | + ``` |
| 72 | + LLEN list |
| 73 | + ``` |
| 74 | +
|
| 75 | +- LINDEX: 获取指定索引上的元素 |
| 76 | + ``` |
| 77 | + LINDEX list index |
| 78 | + ``` |
| 79 | + 正数索引从左端开始算, 起始为0. 负数索引从右端开始算, 起始为-1. 若索引超出范围则返回(nil). |
| 80 | +
|
| 81 | +- LRANGE: 获取给定索引范围上的元素 |
| 82 | + ``` |
| 83 | + LRANGE list start end |
| 84 | + ``` |
| 85 | + 可以使用 `LRANGE list 0 -1` 来获取列表的所有元素 |
| 86 | + - 如果 start 和 end 都超出范围, 则返回空列表 nil |
| 87 | + - 如果其中一个超出索引范围, 则超出范围的起始索引会被修正为0, 超出范围的结束索引会被修正为1. |
| 88 | +
|
| 89 | +### 示例: 分页 |
| 90 | +对于有一定规模的网站来说, 分页程序都是必不可少的; 新闻站点、博客、论坛、搜索引擎等, 都会使用分页程序将数量众多的信息分割为多个页面, 使得用户可以以页面为单位流览网站提供的信息, 并以此来控制网站每次取出的信息数量. |
| 91 | +```Python |
| 92 | +from redis import Redis |
| 93 | +
|
| 94 | +class Paging: |
| 95 | + def __init__(self, client, key): |
| 96 | + self.client = client |
| 97 | + self.key = key |
| 98 | + def add(self, item): |
| 99 | + self.client.rpush(self.key, item) |
| 100 | + def get_page(self, page_number, item_per_page): |
| 101 | + start_index = (page_number - 1) * item_per_page |
| 102 | + end_index = page_number * item_per_page |
| 103 | + return self.client.lrange(self.key, start_index, end_index) |
| 104 | + def size(self): |
| 105 | + return self.client.llen(self.key) |
| 106 | +
|
| 107 | +client = Redis(decode_responses=True) |
| 108 | +topics = Paging(client, "user-topics") |
| 109 | +
|
| 110 | +for i in range(1, 20): |
| 111 | + topics.add(i) |
| 112 | +
|
| 113 | +print(topics.get_page(1, 5)) |
| 114 | +print(topics.get_page(2, 5)) |
| 115 | +print(topics.get_page(1, 10)) |
| 116 | +print(topics.size()) |
| 117 | +``` |
| 118 | + |
| 119 | +- LSET: 为索引设置新元素 |
| 120 | + ``` |
| 121 | + LSET list index new_element |
| 122 | + ``` |
| 123 | + LSET 命令在成功时返回 OK. 若索引范围错误, 返回一个错误 (error) ERR index out of range |
| 124 | +
|
| 125 | +- LINSERT: 将元素插入列表 |
| 126 | + ``` |
| 127 | + LINSERT list BEFORE|AFTER target_element new_element |
| 128 | + ``` |
| 129 | + 该命令第二个参数可以选用 BEFORE 或 AFTER, 用于指示命令将新元素插入目标元素的前面还是后面, 命令完成后返回列表长度. |
| 130 | + 若用户给定的元素不存在 list 中, 则 LINSERT 命令将返回 -1 表示插入失败. |
| 131 | +
|
| 132 | +- LTRIM: 修建列表 |
| 133 | + ``` |
| 134 | + LTRIM list start end |
| 135 | + ``` |
| 136 | + 接受一个列表和一个索引范围, 保留范围内的元素, 删除范围外的所有元素 |
| 137 | +
|
| 138 | +- LREM: 从列表移除指定元素 |
| 139 | + ``` |
| 140 | + LREM list count element |
| 141 | + ``` |
| 142 | + count 决定了移除元素的方式: |
| 143 | + - count = 0, 表示移除列表中包含的所有元素 |
| 144 | + - count > 0, 则从左向右开始检查, 并移除最先发现的 count 个指定的元素 |
| 145 | + - count < 0, 则从右向左开始检查, 并移除最先发现的 abs(count) 个指定的元素 |
| 146 | +
|
| 147 | +### 示例: 代办事项 |
| 148 | +使用两个列表分别记录代办事项和已完成事项: |
| 149 | +- 当用户添加一个新的代办事项时, 程序把这个事项放入代办事项列表中 |
| 150 | +- 当用户完成代办事项中某个事项时, 程序把这个事项从代办列表移除, 并放入已完成事项列表中 |
| 151 | +```Python |
| 152 | +from redis import Redis |
| 153 | +
|
| 154 | +def make_todo_list_key(user_id): |
| 155 | + return user_id + "::todo_list" |
| 156 | +
|
| 157 | +def make_done_list_key(user_id): |
| 158 | + return user_id + "::done_list" |
| 159 | +
|
| 160 | +class TodoList: |
| 161 | + def __init__(self, client, user_id): |
| 162 | + self.client = client |
| 163 | + self.user_id = user_id |
| 164 | + self.todo_list = make_todo_list_key(self.user_id) |
| 165 | + self.done_list = make_done_list_key(self.user_id) |
| 166 | + def add(self, event): |
| 167 | + self.client.lpush(self.todo_list, event) |
| 168 | + def remove(self, event): |
| 169 | + self.client.lrem(self.todo_list, 0, event) # 移除所有元素 |
| 170 | + def done(self, event): |
| 171 | + self.remove(event) |
| 172 | + self.client.lpush(self.done_list, event) |
| 173 | + def show_todo_list(self): |
| 174 | + return self.client.lrange(self.todo_list, 0, -1) |
| 175 | + def show_done_list(self): |
| 176 | + return self.client.lrange(self.done_list, 0, -1) |
| 177 | + def clear(self): |
| 178 | + self.client.delete(make_todo_list_key(self.user_id)) |
| 179 | + self.client.delete(make_done_list_key(self.user_id)) |
| 180 | +
|
| 181 | +client = Redis(decode_responses=True) |
| 182 | +todo = TodoList(client, "peter's todo list") |
| 183 | +
|
| 184 | +todo.add("go to sleep") |
| 185 | +todo.add("buy some milk") |
| 186 | +print("Todo list:", todo.show_todo_list()) |
| 187 | +print() |
| 188 | +
|
| 189 | +todo.done("buy some milk") |
| 190 | +print("Todo list:", todo.show_todo_list()) |
| 191 | +print("Done list:", todo.show_done_list()) |
| 192 | +
|
| 193 | +todo.clear() |
| 194 | +``` |
| 195 | + |
| 196 | +- BLPOP: 阻塞式左端弹出操作 |
| 197 | + ``` |
| 198 | + BLPOP list [list ...] timeout |
| 199 | + ``` |
| 200 | + BLPOP 命令是带有阻塞功能的左端弹出操作, 接受任意个列表, 以及一个秒级精度的超时时限作为参数. |
| 201 | + 该命令会按照从左到右的顺序依次检查用户给定的列表, 并对最先遇到的非空列表执行左端元素弹出操作. |
| 202 | + 如果没有可以执行弹出操作的列表, 则会阻塞该命令, 知道某个给定列表变为非空, 又或者等待时间超出给定的时限为止. |
| 203 | + 若成功执行弹出操作, 则返回一个包含两个元素的列表, 第一个元素记录了执行弹出操作的列表, 即元素来源列表, 第二个参数则是被弹出元素本身. |
| 204 | + - 解除阻塞状态: 如果客户端被阻塞的过程中, 有另一个客户端向导致阻塞的列表推入了新的元素, 那么该列表就会变为非空, 而被阻塞的客户端也会随着 BLOPOP 命令成功弹出列表元素而重新回到非阻塞状态. 如果在同一时间内, 有多个客户端因为同一个列表而被阻塞, 那么当导致阻塞的列表变为非空时, 服务器将按照"先阻塞先服务"的规则, 依次为被阻塞的多个客户端弹出列表元素 |
| 205 | + - 处理空列表: 如果向 BLPOP 命令传入列表都为空列表, 且这些列表在给定时间内都没有变成非空列表, 则会返回一个空值(nil) |
| 206 | + - 列表名的作用: BLPOP 返回来源列表是为了让用户在传入多个列表的情况下, 知道被弹出的元素来源哪个列表 |
| 207 | +
|
| 208 | +- BRPOP: 阻塞式右端弹出操作 |
| 209 | + ``` |
| 210 | + BRPOP list [list ...] timeout |
| 211 | + ``` |
| 212 | + 该命令和 BLPOP 除了方向不同外, 其他都一样 |
| 213 | +
|
| 214 | +- BRPOPLPUSH: 阻塞式弹出并推入操作 |
| 215 | + ``` |
| 216 | + BRPOPLPUSH source target timeout |
| 217 | + ``` |
| 218 | + 若 source 非空, 行为和 RPOPLPUSH 一样, 将 source 的右端弹出, 并推入 target 的左端, 返回弹出的元素 |
| 219 | + 若 source 为空, 该命令将阻塞客户端, 并等待一定的时间, 类似上面的阻塞操作 |
0 commit comments