2025-06-10

一、引言

在 PHP 中,数组(Array)既可以表示 索引数组(下标从 0 开始的有序列表),也可以表示 关联数组(键值对集合)。由于 PHP 底层将“数组”和“哈希表”高度结合,因此它既支持像传统语言那样的“动态数组”,也支持“字典”或“map”式的键值访问。了解 PHP 数组的内部结构与常用操作,不仅能让我们更高效地存储与访问数据,还能在处理大数据量或性能敏感场景时做出更优化的选择。

本文将从以下几个层面展开:

  1. PHP 数组基础:创建、访问、常见用法
  2. 关联数组与多维数组:嵌套、遍历及示例
  3. 底层实现解析:哈希表结构、内存分配与扩容机制(ASCII 图解)
  4. 常用数组操作函数:增、删、改、查、排序及合并
  5. 性能与内存优化技巧:避免不必要的复制、引用传递、SplFixedArray 介绍
  6. 实战示例:动态构建用户列表、缓存数据、分页与搜索
  7. 总结与常见误区

二、PHP 数组基础

2.1 创建与访问

2.1.1 索引数组(Numeric Array)

<?php
// 方式一:使用 array()
$fruits = array('苹果', '香蕉', '橙子');

// 方式二:使用短语法(PHP 5.4+)
$fruits = ['苹果', '香蕉', '橙子'];

// 读取
echo $fruits[0]; // 输出 "苹果"
echo $fruits[1]; // 输出 "香蕉"

// 添加元素(动态扩容)
$fruits[] = '葡萄'; // 相当于 $fruits[3] = '葡萄';

// 遍历
foreach ($fruits as $index => $fruit) {
    echo "{$index} -> {$fruit}\n";
}

解释:

  • PHP 的索引数组默认下标从 0 开始递增,添加新元素时,如果没有给出具体键名,会自动分配下一个可用整型下标。
  • 可以通过 $array[] = $value; 形式来“动态”插入新元素,底层会触发扩容操作(详见第 四 节)。

2.1.2 关联数组(Associative Array)

<?php
// 键值对方式
$user = [
    'id'    => 101,
    'name'  => 'Alice',
    'email' => 'alice@example.com'
];

// 读取
echo $user['name']; // 输出 "Alice"

// 添加或修改
$user['age'] = 28;
$user['email'] = 'alice_new@example.com';

// 遍历
foreach ($user as $key => $value) {
    echo "{$key} => {$value}\n";
}

解释:

  • 关联数组的键可以是字符串,也可以是整型。
  • 底层依然是哈希表(Hash Table),插入时会对“键”进行哈希计算并存储位置。
  • 通过 unset($user['age']); 可以删除某个键值对。

三、关联数组与多维数组

3.1 多维数组示例

<?php
$students = [
    [
        'id'    => 1,
        'name'  => '张三',
        'scores'=> [ '数学'=>95, '英语'=>88 ]
    ],
    [
        'id'    => 2,
        'name'  => '李四',
        'scores'=> [ '数学'=>78, '化学'=>82 ]
    ],
    [
        'id'    => 3,
        'name'  => '王五',
        'scores'=> [ '历史'=>90, '地理'=>85 ]
    ]
];

// 访问示例:第二个学生的英语成绩
echo $students[1]['scores']['英语']; // 输出 88

// 遍历所有学生及其成绩
foreach ($students as $stu) {
    echo "学号:{$stu['id']},姓名:{$stu['name']}\n";
    foreach ($stu['scores'] as $subject => $score) {
        echo "- {$subject}:{$score}\n";
    }
    echo "\n";
}

解释:

  • 多维数组本质上就是“数组的值又是数组”,无需额外申明类型。
  • 访问时使用连续的下标或键即可($arr[x][y])。

3.2 增加与删除子元素

<?php
// 为第一位学生添加“物理”成绩
$students[0]['scores']['物理'] = 92;

// 删除第二位学生的“化学”成绩
unset($students[1]['scores']['化学']);

// 为新学生添加空课程数组
$students[] = [
    'id' => 4,
    'name' => '赵六',
    'scores' => []
];

// 删除整个第三个学生
unset($students[2]);

// 注意:unset 后,$students 数组下标可能不连续
print_r($students);

解释:

  • 使用 unset() 删除会在哈希表中标记该键为已删除槽,后续会被垃圾回收机制清理,但可能在短时间内造成“内存碎片”。
  • 若想“重新索引”索引数组,可在 unset 后使用 array_values() 重建如:$students = array_values($students);

四、底层实现解析(哈希表结构、内存分配与扩容机制)

要高效使用 PHP 数组,了解底层原理至关重要。PHP 数组底层是一个哈希表(Hash Table),对索引数组与关联数组不做明显区分,逻辑一致。下面用 ASCII 图解说明其核心结构。

4.1 哈希表简化示意图

┌───────────────────────────────────────────────┐
│           PHP Hash Table (数组)               │
│───────────────────────────────────────────────│
│  底层存储:                                          │
│    buckets 数组(每个 bucket 包含 key、value、       │
│    hash、next 指针等)                               │
│    bucket 数组大小(capacity)会随元素增多而扩容      │
│    当元素数量接近 capacity * 负载因子(load factor)时 │
│    自动扩容(rehash)                                 │
│                                                   │
│  访问流程:                                         │
│    1. 对 key 进行哈希计算,定位到 buckets 数组下标 idx  │
│    2. 如果 buckets[idx] 的 key 与目标 key 匹配,直接返回  │
│    3. 否则,沿着 next 链表逐个比较,直到找到或未命中       │
│                                                   │
│  删除流程:                                         │
│    1. 定位到 key 所在 bucket,并将其标记为“已删除”      │
│    2. 调整链表 next 指针跳过该 bucket              │
│    3. 实际内存释放延迟,到下次重 Hash 时统一压缩碎片    │
└───────────────────────────────────────────────┘
  • buckets 数组:底层连续内存,每个槽(bucket)存放一个数组元素的 key 的哈希值、key(string 或 int)、value(zval)、next(用于冲突时链表链接)。
  • 负载因子(load factor):PHP 默认在装载因子达到 \~1 时扩容,具体阈值和策略可在不同 PHP 版本中略有差异。
  • 链表处理冲突:若两个不同 key 计算出相同哈希值,会形成“冲突”并将新元素挂到该槽的链表后面。

4.2 动态扩容示意

假设最初的 capacity 为 8(下标 0~7)。插入 9 个元素时,完美哈希将最后一个元素映射到已满之处,需要扩容到下一个质数大小(通常 PHP 选择约 2 倍大小的质数,比如 17),然后将原有元素重新分配到新的 buckets。

初始状态:
capacity = 8
buckets index: 0   1   2   3   4   5   6   7
                ┌───┬───┬───┬───┬───┬───┬───┬───┐
                │   │   │   │   │   │   │   │   │   <- 每格存放若干 bucket
                └───┴───┴───┴───┴───┴───┴───┴───┘

插入 8 个元素后满载:
插入第 9 个元素:
触发扩容,new capacity ≈ 16 或 17(取质数)

扩容后:
capacity = 17
buckets index: 0 … 16
                ┌──┬──┬── … ──┬──┐
                │  │  │    …  │  │
                └──┴──┴── …  ──┴──┘

重新哈希分配原有 8 个元素到 17 个槽中
然后将第九个元素也放入对应位置
  • 扩容成本高:一次性插入大量元素或频繁增长会导致频繁扩容,影响性能。
  • 优化思路:如果事先能知道大概元素数量,可以预先调用 array_fill() 或设置初始大小(例如 SplFixedArray)以减少扩容次数(详见 § 六.2)。

五、常用数组操作函数

PHP 内置了大量数组操作函数,能够快速完成常见增删改查与排序、合并、过滤等操作。下面列出几类常用操作并示例说明。

5.1 增删改查

  • array_push(array &$array, mixed ...$values): int:将一个或多个元素压入数组末尾
  • array_pop(array &$array): mixed:弹出并返回数组末尾元素
  • array_shift(array &$array): mixed:弹出并返回数组开头元素(所有下标会重新索引)
  • array_unshift(array &$array, mixed ...$values): int:在数组开头插入一个或多个元素
  • unset($array[$key]):删除指定键(可针对索引或关联键)
<?php
$data = [1, 2, 3];
array_push($data, 4, 5); // [1,2,3,4,5]
array_pop($data);        // 返回 5,数组变为 [1,2,3,4]
array_shift($data);      // 返回 1,数组变为 [2,3,4](重新索引)
array_unshift($data, 0); // 数组变为 [0,2,3,4]
unset($data[2]);         // 删除索引为 2 的元素,结果:[0,2=>3,4],需要 array_values() 重索引
$data = array_values($data); // 重建索引为 [0,1=>3,2=>4]

5.2 排序与过滤

  • sort(array &$array, int $flags = SORT_REGULAR): bool:对索引数组按值升序排序,重建索引
  • asort(array &$array, int $flags = SORT_REGULAR): bool:对关联数组按值升序排序,保留键名
  • ksort(array &$array, int $flags = SORT_REGULAR): bool:对关联数组按键升序排序
  • array_filter(array $array, callable $callback = null): array:过滤数组,保留回调返回 true 的元素
  • array_map(callable $callback, array ...$arrays): array:对数组每个元素应用回调,返回新数组
<?php
$nums = [3, 1, 4, 1, 5, 9];
sort($nums);        // [1,1,3,4,5,9]

$userages = ['Alice'=>28, 'Bob'=>22, 'Cindy'=>25];
asort($userages);   // ['Bob'=>22, 'Cindy'=>25, 'Alice'=>28]
ksort($userages);   // ['Alice'=>28, 'Bob'=>22, 'Cindy'=>25](按键名升序)

$filtered = array_filter($nums, function($n) {
    return $n > 2;  // 过滤大于 2 的值
});                 // [2=>3,3=>4,4=>5,5=>9],原索引保留,可再 array_values()

$squared = array_map(function($n) {
    return $n * $n;
}, $nums);          // [1,1,9,16,25,81]

5.3 合并与差集交集

  • array_merge(array ...$arrays): array:合并一个或多个数组(索引数组会重建索引,关联数组会覆盖相同键)
  • array_merge_recursive(array ...$arrays): array:类似 array_merge,但当键相同时,值会合并为子数组
  • array_diff(array $array, array ...$arrays): array:返回在第一个数组中但不在其他数组中的元素
  • array_intersect(array $array, array ...$arrays): array:返回所有数组的交集元素
<?php
$a = [1, 2];
$b = [3, 4];
$merged = array_merge($a, $b); // [1,2,3,4]

$arr1 = ['key1'=>'A', 'key2'=>'B'];
$arr2 = ['key2'=>'C', 'key3'=>'D'];
$m = array_merge($arr1, $arr2); // ['key1'=>'A','key2'=>'C','key3'=>'D']

$diff = array_diff([1, 2, 3], [2, 4]); // [0=>1,2=>3]
$inter = array_intersect([1, 2, 3], [2, 3, 5]); // [1=>2,2=>3]

六、性能与内存优化技巧

6.1 避免不必要的复制

PHP 数组是**写时复制(copy-on-write)**的结构。当你将一个数组赋值给另一个变量时,底层并未立即复制内存,只有在“写入”时才真正复制。这意味着:

<?php
$a = [1, 2, 3];
$b = $a;        // 仅复制 zval 引用,内存未复制
$b[0] = 99;     // 这时 PHP 会复制数组数据到新内存

**优化思路:**如果想在函数中处理大数组而不复制,可使用引用传递(&)或在需要修改时先 unset 再操作。

<?php
function processArray(array &$arr) {
    foreach ($arr as &$val) {
        $val = $val * 2;
    }
    unset($val); // 解除引用
}

6.2 SplFixedArray:固定长度数组

当你需要一个拥有固定大小的“数组”并对性能敏感时,可以使用 SplFixedArray,它不会像普通 PHP 数组一样浪费哈希表开销。

<?php
// 创建长度为 1000 的固定数组
$fixed = new SplFixedArray(1000);

// 赋值
for ($i = 0; $i < $fixed->getSize(); $i++) {
    $fixed[$i] = $i * 2;
}

// 读取
echo $fixed[10]; // 20

// 注意:unset 不会改变大小,但会置为 null
unset($fixed[10]);
var_dump($fixed[10]); // NULL

// 转为普通数组(当需要使用数组函数时)
$normal = $fixed->toArray(); // 约为 [0=>0,1=>2,...]
  • 优点:更节省内存、更高效,因为底层并非哈希表,而是简单的连续内存块。
  • 缺点:只支持整数索引,且大小固定,如需改变大小需要 setSize() 重新分配。

6.3 避免深度拷贝与递归

当数组中包含其他数组或对象时,频繁地递归拷贝会带来很大开销:

<?php
function deepCopy(array $arr) {
    $result = [];
    foreach ($arr as $key => $value) {
        if (is_array($value)) {
            $result[$key] = deepCopy($value);
        } elseif (is_object($value)) {
            $result[$key] = clone $value;
        } else {
            $result[$key] = $value;
        }
    }
    return $result;
}
  • 如果不必要,尽量避免手动深拷贝,可以只拷贝最外层,内部用引用或仅复制必要字段。
  • 在调用频繁、数据量大的场景,考虑使用 SplFixedArray 或数据库直接操作而非内存级拷贝。

七、实战示例:动态构建用户列表及分页搜索

下面通过一个完整示例,演示如何用 PHP 数组实现“用户列表”的动态构建、分页、搜索及优化思路。

7.1 示例需求

  • 从数据库或模拟数据源中获取大量用户数据(假设 10000 条)。
  • 根据页面传入的 pagesize 参数,动态分页并返回子数组。
  • 根据 keyword 参数对用户名或邮箱进行模糊搜索,返回搜索后的分页结果。
  • 缓存热门页面结果,降低数据库压力。

7.2 模拟数据源

<?php
// data.php
function generateUsers($count = 10000) {
    $users = [];
    for ($i = 1; $i <= $count; $i++) {
        $users[] = [
            'id'    => $i,
            'name'  => "User{$i}",
            'email' => "user{$i}@example.com"
        ];
    }
    return $users;
}

7.3 用户列表与分页逻辑

<?php
// UserController.php
require 'data.php';
require 'vendor/autoload.php';

use App\Cache\ApcuCache;

class UserController {
    private $users;
    private $cache;

    public function __construct() {
        // 模拟从数据库获取大量用户
        $this->users = generateUsers();
        $this->cache = new ApcuCache();
    }

    /**
     * 列表接口:分页 + 可选搜索
     * @param int $page 当前页,默认1
     * @param int $size 每页条数,默认20
     * @param string $keyword 搜索关键字
     * @return array 包含 total、data
     */
    public function list($page = 1, $size = 20, $keyword = '') {
        $page = max(1, (int)$page);
        $size = max(1, min(100, (int)$size)); // 限制 size 在 1~100 之间
        $keyword = trim($keyword);

        // 构建缓存键:带搜索关键字,否则分页后的结果不同
        $cacheKey = "user_list_{$page}_{$size}_" . ($keyword ?: 'all');

        // 先尝试从 APCu 缓存读取
        $cached = $this->cache->get($cacheKey);
        if ($cached !== null) {
            return $cached;
        }

        // 如果有关键词,则先过滤数组
        if ($keyword !== '') {
            $filtered = array_filter($this->users, function($user) use ($keyword) {
                return stripos($user['name'], $keyword) !== false
                    || stripos($user['email'], $keyword) !== false;
            });
        } else {
            $filtered = $this->users;
        }

        $total = count($filtered);
        $offset = ($page - 1) * $size;

        // array_slice 保留原索引,如果需要重建索引可传入第三个参数 true
        $data = array_slice($filtered, $offset, $size, true);

        $result = [
            'total' => $total,
            'page'  => $page,
            'size'  => $size,
            'data'  => array_values($data) // 重建索引
        ];

        // 缓存 60 秒
        $this->cache->set($cacheKey, $result, 60);

        return $result;
    }
}

// 简易路由逻辑
$page    = $_GET['page'] ?? 1;
$size    = $_GET['size'] ?? 20;
$keyword = $_GET['keyword'] ?? '';

$controller = new UserController();
$response = $controller->list($page, $size, $keyword);

header('Content-Type: application/json');
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

7.3.1 关键点说明

  1. 搜索过滤:使用 array_filter() 遍历完整用户数组(长度 10000),复杂度 O(n),在一次请求内可能带来性能开销。

    • 可优化思路:如果搜索频繁,可考虑全文索引(如 MySQL LIKE、Elasticsearch 等)而不是纯内存循环。
  2. 分页截取array_slice() 会复制子数组,空间复杂度 O(k),其中 k = sizesize 最大为 100,可接受。
  3. 缓存分页结果:将最终的分页结果(包含 totaldata)缓存 60 秒,后续请求相同 page/size/keyword 时直接命中 APCu。

    • 如果搜索关键词非常多或翻页很多,也会产生大量缓存键,需定期清理或限制缓存内容。
  4. 索引重建array_slice() 如果不传第四个参数,默认保留原数组的键;调用 array_values() 重建从 0 开始的连续索引,方便前端直接读取。

7.3.2 流程示意图(ASCII)

┌──────────────────────────────────────────────────┐
│    客户端发起请求 GET /users?page=2&size=20&    │
│    keyword=Alice                                  │
└──────────────────────────────────────────────────┘
                           ↓
┌──────────────────────────────────────────────────┐
│ 1. 构建缓存键 key = "user_list_2_20_Alice"        │
│ 2. 调用 ApcuCache::get(key)                     │
│    ├─ 缓存命中?                                │
│    │   ├─ 是 → 直接返回缓存数据                   │
│    │   └─ 否 → 继续下一步                         │
└──────────────────────────────────────────────────┘
                           ↓
┌──────────────────────────────────────────────────┐
│ 3. 在 $this->users(10000 人)中进行 array_filter  │
│    筛选 name/email 包含 "Alice" 的用户           │
│ 4. 得到 $filtered(如 50 人)                     │
│ 5. 计算 $total = count($filtered)                 │
└──────────────────────────────────────────────────┘
                           ↓
┌──────────────────────────────────────────────────┐
│ 6. $offset = (2-1)*20 = 20;                     │
│ 7. $data = array_slice($filtered, 20, 20)        │
│    → 拿出第 21~40 人的数据                        │
│ 8. 重建索引 array_values($data)                  │
└──────────────────────────────────────────────────┘
                           ↓
┌──────────────────────────────────────────────────┐
│ 9. $result = [ 'total'=>50, 'page'=>2, ... ]      │
│10. 缓存 $result 到 APCu(TTL=60)                 │
│11. 返回 JSON 响应给客户端                         │
└──────────────────────────────────────────────────┘

八、常见误区与注意事项

8.1 误区一:数组越大访问就越慢?

  • 事实:PHP 数组是基于哈希表的,查找、插入、删除等操作的平均时间复杂度约为 O(1),而非线性扫描。
  • 误区原因:在遍历整个数组(如 foreacharray_filter)时,操作时间与数组大小成线性关系,但单次随机访问无关数组大小。
  • 结论:频繁 foreach 大数组会影响性能;但对单个索引或关联键访问,速度并不会因数组增大而显著下降。

8.2 误区二:unset 后 PHP 会立即回收内存?

  • 事实unset($array[$key]) 会在哈希表中标记该槽为“已删除”,但不会立即压缩底层 buckets 或释放物理内存。
  • 影响:若反复插入、删除大量元素,会导致哈希表内部出现碎片,虽然有效元素少,但哈希表容量仍较大。
  • 建议:在适当时机可以调用 array_values() 重建索引数组,或通过 apc_clear_cache() / 重新启动进程来彻底释放内存。

8.3 误区三:使用引用能无限制地节省内存?

  • 事实:引用(&)能避免复制,但也会增加代码复杂度,容易引发“悬空引用”或“循环引用”问题。
  • 注意:在使用 foreach ($arr as &$val) 时,务必在循环结束后 unset($val) 以解除引用,否则后续操作可能改变原数组元素。
  • 示例陷阱

    <?php
    $a = [1, 2, 3];
    foreach ($a as &$v) {
        $v *= 2;
    }
    // 此时 $v 仍然引用最后一个元素
    $b = [4, 5, 6];
    foreach ($b as $val) {
        echo $v; // 可能会意外修改 $a[2]
    }

    必须写成:

    foreach ($a as &$v) { ... }
    unset($v); // 解除引用

8.4 注意 SplFixedArray 与常规数组的区别

  • SplFixedArray 底层使用连续内存,更节省空间且访问更快,但不支持键名为字符串或稀疏索引。
  • 如果需要随机访问大量纯整数索引数据,并且下标范围可以预估,优先考虑 SplFixedArray

九、总结

本文全面、系统地解析了 PHP 动态数组(实际上是哈希表)的存储与访问原理,并结合代码示例与 ASCII 图解,讲解了如下要点:

  1. PHP 数组基础:索引数组与关联数组的创建、访问、遍历与动态插入/删除。
  2. 多维与嵌套数组:如何构建、访问和修改多层嵌套结构。
  3. 底层实现原理:哈希表结构、buckets、链表冲突解决、动态扩容机制(ASCII 示意)。
  4. 常用数组函数:增、删、改、查;排序、过滤、合并、差集与交集等。
  5. 性能与内存优化:写时复制(CoW)、引用传递、SplFixedArray、避免深度拷贝。
  6. 实战示例:用户列表分页、搜索及 APCu 缓存示例,完整流程与性能思考。
  7. 常见误区与注意:遍历 vs 读取性能、unset 内存回收、引用陷阱等。
2024-09-09

Redis 中的数据结构包括:字符串、哈希表、列表、集合、有序集合。

  1. 字符串(String)

    字符串是 Redis 最基本的数据类型。




SET key value  # 设置字符串值
GET key       # 获取字符串值
  1. 哈希表(Hash)

    哈希表用于存储键值对集合。




HSET hash field value  # 设置哈希表字段
HGET hash field       # 获取哈希表字段值
  1. 列表(List)

    列表用于按插入顺序保存字符串值。




LPUSH list value  # 在列表头部插入值
LRANGE list start stop  # 获取列表指定范围内的元素
  1. 集合(Set)

    集合是字符串的无序集合。




SADD set member  # 向集合添加一个成员
SMEMBERS set    # 获取集合中的所有成员
  1. 有序集合(Sorted Set)

    有序集合是字符串的有序集合。




ZADD sortedSet score member  # 向有序集合添加成员
ZRANGE sortedSet start stop [WITHSCORES]  # 获取有序集合指定范围内的成员

以上代码是 Redis 命令的示例,并未提供完整的解决方案,因为需要根据具体问题进行定制。

2024-09-09

Redis底层数据结构主要有:

  1. 字符串(String)
  2. 字典(Hash)
  3. 链表(List)
  4. 集合(Set)
  5. 有序集合(Sorted Set,或称为zset)

这些数据结构都可以用作Redis的键和值。

以下是Redis中这些数据结构的简单实现:

  1. 字符串(String):



// 简单实现一个字符串结构
typedef struct {
    char *str;
    size_t len;
} SimpleString;
 
// 设置字符串
void setString(SimpleString *str, const char *data, size_t len) {
    str->str = malloc(len);
    memcpy(str->str, data, len);
    str->len = len;
}
 
// 获取字符串
void getString(SimpleString *str, char *buf, size_t len) {
    memcpy(buf, str->str, str->len);
}
  1. 字典(Hash):



// 简单实现一个字典结构
typedef struct {
    char *key;
    SimpleString value;
} SimpleHashEntry;
 
typedef struct {
    SimpleHashEntry *entries;
    int size;
} SimpleHash;
 
// 设置字典中的值
void setHashValue(SimpleHash *hash, const char *key, const char *value) {
    for (int i = 0; i < hash->size; i++) {
        if (strcmp(hash->entries[i].key, key) == 0) {
            setString(&hash->entries[i].value, value, strlen(value));
            return;
        }
    }
    // 如果键不存在,则添加键值对
    SimpleHashEntry newEntry = {strdup(key), {NULL, 0}};
    setString(&newEntry.value, value, strlen(value));
    hash->entries = realloc(hash->entries, (hash->size + 1) * sizeof(SimpleHashEntry));
    hash->entries[hash->size++] = newEntry;
}
 
// 获取字典中的值
void getHashValue(SimpleHash *hash, const char *key, char *buf) {
    for (int i = 0; i < hash->size; i++) {
        if (strcmp(hash->entries[i].key, key) == 0) {
            getString(&hash->entries[i].value, buf, hash->entries[i].value.len);
            return;
        }
    }
    // 如果键不存在,则返回空字符串
    buf[0] = '\0';
}
  1. 链表(List):



// 简单实现一个链表结构
typedef struct ListNode {
    char *value;
    struct ListNode *next;
} ListNode;
 
typedef struct {
    ListNode *head;
    ListNode *tail;
    int length;
} SimpleList;
 
// 在链表尾部添加元素
void pushToList(SimpleList *list, const char *value) {
    ListNode *newNode = malloc(sizeof(ListNode));
    newNode->value = strdup(value);
    newNode->next = NULL;
 
    if (list->length == 0) {
        list->head = list->tail = newNode;
    } else {
        list->tail->next = newNode;
        list->tail = newNode;
    }
    list->length++;
}
 
// 从链表头部移除元素
void popFromList(SimpleList *list, char *buf) {
    if (list->length == 0) {
        buf[0
2024-09-09

Redis 的 List 数据结构是一个字符串链表,你可以在这个链表的头部或尾部添加或删除元素。这使得 List 既可以作为栈,也可以作为队列使用。

以下是 Redis List 数据结构的常用命令:

  • LPUSH key value[ value ...]:在 key 对应的 list 的头部添加元素,可以添加一个或多个元素。
  • RPUSH key value[ value ...]:在 key 对应的 list 的尾部添加元素,可以添加一个或多个元素。
  • LRANGE key start stop:返回 key 对应的 list 中指定区间内的元素,区间通过 start 和 stop 指定,-1 表示最后一个元素。
  • LPOP key:移除并返回 key 对应的 list 的头部元素。
  • RPOP key:移除并返回 key 对应的 list 的尾部元素。

实例代码:




import redis
 
# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)
 
# 在 list 头部添加元素
r.lpush('mylist', 'element1')
r.lpush('mylist', 'element2', 'element3')
 
# 在 list 尾部添加元素
r.rpush('mylist', 'element4')
r.rpush('mylist', 'element5', 'element6')
 
# 获取 list 中的所有元素
print(r.lrange('mylist', 0, -1))  # 输出:['element6', 'element5', 'element4', 'element3', 'element2', 'element1']
 
# 移除并返回 list 的头部元素
print(r.lpop('mylist'))  # 输出:element6
 
# 移除并返回 list 的尾部元素
print(r.rpop('mylist'))  # 输出:element1

以上代码演示了如何在 Python 中使用 redis 模块操作 Redis 的 List 数据结构。

2024-09-09

Redis底层使用了一系列的数据结构来存储数据,这些数据结构包括:字符串、链表、字典、跳表、紧凑列表、散列表等。

  1. 字符串:Redis中的字符串是可以修改的,当一个字符串小于等于39字节时,Redis会使用embstr编码方式存储,否则使用raw编码方式。
  2. 链表:Redis的链表是双端列表,可以在O(1)时间内完成插入和删除操作。
  3. 字典:Redis的字典是一个键值对集合,内部结构使用哈希表实现,解决键的冲突使用链地址法。
  4. 跳表:Redis的跳表是一种可以进行二分查找的有序数据结构,每一层都是一个有序链表,可以在O(logN)时间内完成查找操作。
  5. 紧凑列表:Redis的紧凑列表是一种为了节省内存而开发的特殊编码的链表,它会将连续的小整数值压缩存储。
  6. 散列表:Redis的散列表是一个包含键值对的数组,数组的每个元素都是一个链表,解决键的冲突使用链地址法。

以下是一个简单的Redis键值对示例,它使用了字符串、字典和散列表:




// 假设这是Redis中的一个键值对
struct redisObject {
    int type; // 对象类型
    void *ptr; // 指向实际数据的指针
    // ... 其他属性
};
 
// 字符串对象
struct redisStringObject {
    int len; // 字符串长度
    char *buf; // 字符串缓冲区
};
 
// 字典对象
struct redisDict {
    dict *dict; // 哈希表
};
 
// 散列表对象
struct redisHash {
    dict *dict; // 哈希表,每个键值对又是一个字典
};
 
// 假设这是一个键为"mykey",值为"myvalue"的键值对
struct redisObject key = {"string", "mykey"};
struct redisObject value = {"hash", createHashObject()};
 
// 创建散列表对象
struct redisHash *createHashObject() {
    struct redisHash *hash = malloc(sizeof(struct redisHash));
    hash->dict = dictCreate();
    dictAdd(hash->dict, "myfield", "myvalue");
    return hash;
}
 
// 假设这是一个Redis命令:HSET mykey myfield myvalue

在这个例子中,"mykey"是一个字符串对象,"myvalue"是一个散列表对象。"myfield"和"myvalue"是散列表对象中的键值对,它们分别是字符串和字典对象。

2024-09-09



from django.db import models
 
# 用户信息模型
class UserInfo(models.Model):
    username = models.CharField(max_length=30)
    email = models.EmailField(max_length=50)
    age = models.IntegerField()
    # 自定义__str__方法以便打印对象时显示用户名
    def __str__(self):
        return self.username
 
# 文章信息模型
class Article(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    author = models.ForeignKey(UserInfo, on_delete=models.CASCADE)
    publish_time = models.DateTimeField(auto_now_add=True)
    # 自定义__str__方法以便打印对象时显示文章标题
    def __str__(self):
        return self.title

这段代码定义了两个Django模型:UserInfoArticleUserInfo模型用于存储用户信息,包括用户名、电子邮件和年龄。Article模型用于存储文章信息,包括标题、内容和作者信息,并且每篇文章与一个用户(作者)关联。这些模型都使用了适当的字段类型,并且展示了如何通过ForeignKey字段定义模型之间的关系。通过这样的定义,开发者可以轻松地创建、查询和管理用户及文章数据。

2024-09-09

在Laravel框架中,我们可以使用其内置的数据库迁移功能(migration)来修改数据库结构,而不需要依赖于doctrine/dbal组件。以下是一个简单的例子,展示如何创建一个新的迁移来修改数据库结构。

首先,我们需要创建一个新的迁移文件。在命令行中,运行以下命令:




php artisan make:migration modify_some_table_structure --table=some_table

这将会创建一个新的迁移文件,在database/migrations目录下。接下来,在这个迁移文件中,我们可以使用Schema门面来修改表结构。




use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
class ModifySomeTableStructure extends Migration
{
    public function up()
    {
        Schema::table('some_table', function (Blueprint $table) {
            // 添加新列
            $table->string('new_column')->after('some_column');
            
            // 修改现有列
            $table->text('existing_column')->nullable()->change();
            
            // 删除列
            $table->dropColumn('old_column');
        });
    }
 
    public function down()
    {
        Schema::table('some_table', function (Blueprint $table) {
            // 撤销up方法中的所有操作
            $table->dropColumn('new_column');
            $table->string('existing_column')->change();
            $table->string('old_column')->nullable(false); // 假设原来列不允许null
        });
    }
}

最后,运行迁移来应用这些更改:




php artisan migrate

这个迁移操作将会在some_table表上添加一个新列new_column,修改existing_column列使其可为null,并删除old_column列。down方法提供了一个撤销这些更改的途径。这个迁移过程不需要doctrine/dbal组件,因为Laravel的Schema门面已经提供了所需的数据库抽象层。

2024-09-09

在Redis中,Zset(Sorted Set)是一种数据类型,它不仅存储元素,而且还将每个元素关联到一个浮点数的分数。Zset中的成员是唯一的,但分数可以重复。

Redis的Zset底层实现了一个跳跃列表(skiplist),同时为了保证数据结构的正确性,它还引入了一个哈希表。

跳跃列表是一种平衡的数据结构,它通过多级链表的方式来保证数据的有序,每个节点都可能有多个指针指向后续的节点。

哈希表用于确保成员的唯一性,它的作用是在期望的时间内,根据成员查找或者更新相关的分数。

下面是一个简单的示例,描述了Zset在Redis中的存储逻辑:




typedef struct zskiplistNode {
    robin_hood::unordered_map::node* ht_node; // 指向哈希表的节点
    struct zskiplistNode* backward; // 后退指针
    double score; // 分数
    robj* obj; // 成员对象指针
    struct zskiplistNode* forward; // 前进指针
    unsigned int span; // 跳跃范围
} zskiplistNode;
 
typedef struct zskiplist {
    struct zskiplistNode* header, * tail; // 表头和表尾节点
    unsigned long length; // 节点的数量
    int level; // 最高层数
} zskiplist;
 
typedef struct zset {
    dict* dict; // 哈希表,用于保存成员到分数的映射
    zskiplist* zsl; // 跳跃列表,用于保存有序的成员列表
} zset;

在这个结构中,zset包含了一个zskiplist和一个dict。zskiplist用于保存成员按分数排序的列表,而dict用于快速查找成员对应的分数。

当你要添加、删除或者查找元素时,Redis会根据成员在跳跃列表中的位置来更新哈希表,并且可以在对数平均时间内完成操作,保证了操作的高效性。

2024-09-09

Redis 是一个开源的使用 C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。

Redis 的基础数据结构包括字符串、列表、集合、哈希表和有序集合。

  1. 字符串(String)

Redis 字符串是简单的 key-value 类型,value 最大能存储 512 MB。




# 设置 key-value
redis.set('key', 'value')
 
# 获取 key 对应的 value
redis.get('key')
  1. 列表(List)

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。




# 在列表左侧添加元素
redis.lpush('key', 'value')
 
# 在列表右侧添加元素
redis.rpush('key', 'value')
 
# 获取列表的全部元素
redis.lrange('key', 0, -1)
  1. 集合(Set)

Redis 的集合是无序的字符串集合。你可以添加、删除元素,还可以求交集、并集、差集。




# 添加元素
redis.sadd('key', 'value')
 
# 获取集合的所有元素
redis.smembers('key')
  1. 哈希表(Hash)

Redis 的哈希表是键值对的集合。




# 设置单个属性
redis.hset('key', 'field', 'value')
 
# 获取所有属性
redis.hgetall('key')
  1. 有序集合(Sorted Set)

Redis 的有序集合是具有分数的有序字符串集合,分数可以用来排序。




# 添加元素
redis.zadd('key', {'value': score})
 
# 获取排序后的所有元素
redis.zrange('key', 0, -1)

Redis 的应用场景广泛,以下是一些常见的使用场景:

  • 缓存系统
  • 排行榜
  • 消息队列系统
  • 分布式锁
  • 会话共享
  • 网站访问统计

以上是 Redis 的基础数据结构和常见应用场景,具体使用时需要根据实际需求选择合适的数据结构和命令。

2024-09-06

SDS,即简单动态字符串(Simple Dynamic String),是Redis中的一种数据结构,用于保存字符串。它是二进制安全的,并且是可修改的,这意味着它可以用于保存任何类型的数据,包括二进制数据。

以下是一个简单的C语言实现的SDS数据结构的例子:




#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef struct sdshdr {
    // 记录buf数组中已使用字节的数量
    // 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
} sdshdr;
 
// 创建一个新的sds
sdshdr *sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    sdshdr *sh;
 
    // 根据需要保存的字符串长度,分配足够的空间
    // 这里加8是为了分配sdshdr和字符串内容之后的空间
    sh = malloc(sizeof(sdshdr) + initlen + 1);
    if (sh == NULL) return NULL;
 
    // 初始化sdshdr的属性
    sh->len = initlen;
    sh->free = 0;
 
    // 如果有初始化字符串,则复制到buf中
    if (initlen > 0) {
        memcpy(sh->buf, init, initlen);
    }
 
    // 字符串结尾设置空字符
    sh->buf[initlen] = '\0';
    return sh;
}
 
// 打印sds中的字符串
void sdsprint(const sdshdr *sh) {
    printf("%s\n", sh->buf);
}
 
// 释放sds占用的内存
void sdsfree(sdshdr *sh) {
    free(sh);
}
 
int main() {
    // 创建一个新的sds,包含字符串"Hello, Redis!"
    sdshdr *sh = sdsnew("Hello, Redis!");
    if (sh == NULL) {
        printf("Out of memory\n");
        return 1;
    }
 
    // 打印sds中的字符串
    sdsprint(sh);
 
    // 释放sds占用的内存
    sdsfree(sh);
    return 0;
}

这个简单的实现展示了如何创建一个新的sds,如何打印它包含的字符串,以及如何释放它占用的内存。注意,这只是一个示例实现,Redis中的SDS实现要复杂得多,包含了性能优化和其他功能。