美文网首页
Redis-01数据类型之string类型与list类型

Redis-01数据类型之string类型与list类型

作者: 安晓生 | 来源:发表于2021-11-30 14:23 被阅读0次
    • 1,string数据类型与结构
    • 2,string数据类型应用场景
    • 3,list数据类型与结构
    • 4,list数据类型应用场景

    1 Redis介绍总结

    redis特点

    • 1.速度快

    为啥快呢?
    官方给出的数字是读写性能可以达到10万/秒,当然这也取决于机器的性能,但这里先不讨论机器性能上的差异,只分析一下是什么造就了Redis除此之快的速度,可以大致归纳为以下三点:

    1. Redis的所有数据都是存放在内存中的,所以把数据放在内存中是Redis速度快的最主要原因。
    2. Redis是用C语言实现的,一般来说C语言实现的程序“距离”操作系统更近,执行速度相对会更快。
    3. Redis使用了单线程架构,预防了多线程可能产生的竞争问题。
    • 2.简单稳定

    Redis的简单主要表现在三个方面。

    1. Redis的源码很少。
    2. Redis使用单线程模型,这样不仅使得Redis服务端处理模型变得简单,而且也使得客户端开发变得简单。
    3. Redis不需要依赖于操作系统中的类库(例如Memcache需要依赖libevent这样的系统类库),Redis自己实现了事件处理的相关功能。
      Redis虽然很简单,但是不代表它不稳定。维护的上千个Redis为例,没有出现过因为Redis自身bug而宕掉的情况。
    • 3.语言多

    Redis提供了简单的TCP通信协议,很多编程语言可以很方便地接入到Redis,并且由于Redis受到社区和各大公司的广泛认可,所以支持Redis的客户端语言也非常多,几乎涵盖了主流的编程语言,例如Java、PHP、Python、C、C++、Nodejs等。

    redis的功能

    1.多种数据类型
    2.redis持久化
    3.redis主从复制
    4.redis哨兵
    5.redis集群

    1. string数据类型与结构

    1.1 string类型简介

    String类型是redis的最基础的数据结构,也是最经常使用到的类型。而且其他的四种类型多多少少都是在字符串类型的基础上构建的,所以String类型是redis的基础。
    string 类型的值最大能存储 512MB,这里的String类型可以是简单字符串、复杂的xml/json的字符串、二进制图像或者音频的字符串、以及可以是数字的字符串。

    • 1.set 命令
      描述:该命令用于设置给定 key 的值。如果 key 已经存储其他值, SET 就覆写旧值,且无视类型
    • 2.get 命令
      描述:该命令用于获取指定 key 的值。如果 key不存在,返回 nil 。如果key对应储存的值不是字符串类型,返回一个错误。
    • 3.getset 命令
      描述:该命令用于获取指定的key的旧值,然后按照新值对key进行赋值。当key中没有旧值的时候返回nil。
    • 4.mget 命令
      描述:该命令用于返回多个key的值,当其中某一个KEY的值不存在,返回nil
    • 5.decr 命令
      描述:对key对应的数字做减1操作。如果key不存在,那么在操作之前,这个key对应的值会被置为0。如果key有一个错误类型的value或者是一个不能表示成数字的字符串,就返回错误
    • 6.incr 命令
      描述:对存储在指定key的数值执行原子的加1操作,如果指定的key不存在,那么在执行incr操作之前,会先将它的值设定为0。如果指定的key中存储的值不是字符串类型(fix:)或者存储的字符串类型不能表示为一个整数,那么执行这个命令时服务器会返回一个错误(eq:(error) ERR value is not an integer or out of range)。

    1.2 string简单字符结构

    1.2.1 SDS动态字符串

    SDS(Simple Dynamic Strings, 简单动态字符串)是 Redis 的一种基本数据结构,主要是用于存储字符串和整数。

    SDS数据结构实现(Redis3):

    struct sdshdr {
        unsigned int len;
        unsigned int free;
        char buf[];
    };
    

    其中,buf 表示数据空间,用于存储字符串;len 表示 buf 中已占用的字节数,也即字符串长度;free 表示 buf 中剩余可用字节数。

    好处

    • 用单独的变量 len 和 free,可以方便地获取字符串长度和剩余空间;
    • 内容存储在动态数组 buf 中,SDS 对上层暴露的指针指向 buf,而不是指向结构体 SDS。因此,上层可以像读取 C 字符串一样读取 SDS 的内容,兼容 C 语言处理字符串的各种函数,同时也能通过 buf 地址的偏移,方便地获取其他变量;
    • 读写字符串不依赖于 \0,保证二进制安全。

    坏处

    • 对于不同长度的字符串,没有必要使用 len 和 free 这 2 个 4 字节的变量?
    • 4 字节的 len,可表示的字符串长度为 2^32,而在实际应用中,存放于 Redis 中的字符串往往没有这么长,因此,空间的使用上能否进一步压缩?
    1.2.2 新的SDS结构

    Redis 增加了一个 flags 字段来标识类型,用一个字节(8 位)来存储。

    其中:前 3 位表示字符串的类型;剩余 5 位,可以用来存储长度小于 32 的短字符串。

    struct __attribute__ ((__packed__)) sdshdr5 {
        unsigned char flags; /* 前3位存储类型,后5位存储长度 */
        char buf[]; /* 动态数组,存放字符串 */
    };
    

    而对于长度大于 31 的字符串,仅仅靠 flags 的后 5 位来存储长度明显是不够的,需要用另外的变量来存储。
    sdshdr8、sdshdr16、sdshdr32、sdshdr64 的数据结构定义如下,其中 :

    • len 表示已使用的长度
    • alloc 表示总长度
    • buf 存储实际内容
    • flags 的前 3 位依然存储类型,后 5 位则预留。
    struct __attribute__ ((__packed__)) sdshdr8 {
        uint8_t len; /* 已使用长度,1字节 */
        uint8_t alloc; /* 总长度,1字节 */
        unsigned char flags; /* 前3位存储类型,后5位预留 */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr16 {
        uint16_t len; /* 已使用长度,2字节 */
        uint16_t alloc; /* 总长度,2字节 */
        unsigned char flags; /* 前3位存储类型,后5位预留 */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr32 {
        uint32_t len; /* 已使用长度,4字节 */
        uint32_t alloc; /* 总长度,4字节 */
        unsigned char flags; /* 前3位存储类型,后5位预留 */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr64 {
        uint64_t len; /* 已使用长度,8字节 */
        uint64_t alloc; /* 总长度,8字节 */
        unsigned char flags; /* 前3位存储类型,后5位预留 */
        char buf[];
    };
    

    Redis创建字符串流程

    sds sdsnewlen(const void *init, size_t initlen) {
        void *sh;
        sds s;
        // 根据字符串长度计算相应类型
        char type = sdsReqType(initlen);
        // 如果创建的是""字符串,强转为SDS_TYPE_8
        if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
        // 根据类型计算头部所需长度(头部包含 len、alloc、flags)
        int hdrlen = sdsHdrSize(type);
        // 指向flags的指针
        unsigned char *fp;
        // 创建字符串,+1是因为 `\0` 结束符
        sh = s_malloc(hdrlen+initlen+1);
        if (sh == NULL) return NULL;
        if (init==SDS_NOINIT)
            init = NULL;
        else if (!init)
            memset(sh, 0, hdrlen+initlen+1);
        // s指向buf
        s = (char*)sh+hdrlen;
        // s减1得到flags
        fp = ((unsigned char*)s)-1;
        ...
        // 在s末尾添加\0结束符
        s[initlen] = '\0';
        // 返回指向buf的指针s
        return s;
    }
    

    创建 SDS 的大致流程是这样的:首先根据字符串长度计算得到 type,根据 type 计算头部所需长度,然后动态分配内存空间。
    注意:

    • 创建空字符串时,SDS_TYPE_5 被强制转换为 SDS_TYPE_8(原因是创建空字符串后,内容可能会频繁更新而引发扩容操作,故直接创建为 sdshdr8)
    • 长度计算有 +1 操作,因为结束符 \0 会占用一个长度的空间。
    • 返回的是指向 buf 的指针 s。

    2. string数据类型的应用

    2.1 session共享

    image.png
    一个分布式web服务将用户的Session信息(比如:登录信息)记录到各自服务器中,这样会出现一个问题,在负载均衡的情况下,服务器会将用户的访问均衡到不同的服务器上,用户刷新一次访问可能就会发现需要重新登录,这个问题对于用户体验来说是无法容忍的。

    为了解决这个问题我们会是使用Redis将用户的Session进行集中管理,这样就只需要保证redis的高可用以及扩展性,每次用户的登录或者查询登录都从Redis中获取Session信息。


    image.png

    2.2 计数器

    2.2.1 商品浏览记录
    <?php
    $config = include "../config/database.php";
    $Redis = new Redis();
    $Redis->connect($config['redis']['host'],$config['redis']['port']);
    $Redis->auth($config['redis']['password']);
    $key = "product:".$_GET['product_id'];
    if (!$Redis->exists($key)){
        $Redis->set($key,1);
    //    header("Location:http://blog-login.com/public/product_key.php?id=".$_GET['product_id']);
        header("Location:http://blog-login.com/view/SteelSeries_zxr.php?id=".$_GET['id']."&product_id=1");
    }else{
        $Redis->incr($key);
    //    header("Location:http://blog-login.com/public/product_key.php?id=".$_GET['product_id']);
        header("Location:http://blog-login.com/view/SteelSeries_zxr.php?id=".$_GET['id']."&product_id=1");
    }
    ?>
    

    2.3 Redis限速

    在一些项目中为了保证安全会要求用户在登录的时候输入手机号进行验证码验证,但是为了保证短信接口不被频繁访问,会进行一定的限制。

    <?php
    $Redis = new Redis("192.168.29.108",6379);
    $Redis->auth("root");
    $phonename="183xxxx2888";
    $id = 1;
    $key = "user:$id:info:".$phonename;
    
    $restful = $Redis->exists($key);
    if ($restful != null || $Redis->incr($key)<=5) {
      return "OK";
    }else {
      echo "1分钟不能请求5次";
    }
    ?>
    

    上面的代码就是使用Redis实现了限速的功能,例如一些网站限制一个IP地址不能在1秒内访问超过n次也可以使用类似的思路。

    3. list数据类型与结构

    3.1 list数据类型

    list类型是用来存储多个有序的字符串的,列表当中的每一个字符看做一个元素,一个列表当中可以存储有一个或者多个元素,redis的list支持存储2^32次方-1个元素。redis可以从列表的两端进行插入(pubsh)和弹出(pop)元素,支持读取指定范围的元素集,或者读取指定下标的元素等操作。redis列表是一种比较灵活的链表数据结构,它可以充当队列或者栈的角色。

    redis列表是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可以重复的。意味着它可以根据链表的下标获取指定的元素和某个范围内的元素集。

    常用命令

      1. Lpush 命令
        描述:将一个或多个值插入到列表头部。 如果 key 不存在,则创建list,然后再插入数据操作。 当 key 存在但不是列表类型时,返回一个错误。
      1. Blpop 命令
        描述:redis的list是链表结构所以BLPOP命令正是取出列表的第一个元素,如果list当中没有没有元素,会一直等待到超时,或者发现有数据为止。
      1. Linsert 命令
        描述:指的是在list列表的某一个元素前或者后插入另外一个元素。当指的的元素不存在时,不执行任何动作。如果列表不存在时,视为空列表,不执行任何动作。
      1. Lindex 命令
        描述:用于通过链表的下标索引获取列表中的元素。这里的下标也可以是负数表示list的最后一个元素,-2表示倒数第二个元素。
      1. Llen 命令
        描述:用于返回list列表的长度。 假如list不存在,则 list 被解释为一个空列表,返回 0 。
      1. Lrange命令
        描述:于返回指定list区间内的元素。区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。

    4. list类型应用场景

    4.1 队列-秒杀抢购

    list类型的lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能,故而可以用Redis的list类型实现简单的点对点的消息队列。不过我不推荐在实战中这么使用,因为现在已经有Kafka、NSQ、RabbitMQ等成熟的消息队列了,它们的功能已经很完善了,除非是为了更深入地理解消息队列,不然我觉得没必要去重复造轮子。

    数据表
    秒杀商品表(product_seckill):

    CREATE TABLE `product_seckill` (
      `id` int NOT NULL,   --主键id
      `product_id` int NOT NULL, --商品id
      `start_at` datetime DEFAULT NULL, --活动开始时间
      `stop_at` datetime DEFAULT NULL,  --活动结束时间
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    

    商品表(product):

    CREATE TABLE `products` (
      `id` int NOT NULL AUTO_INCREMENT, --主键id
      `title` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, --商品标题
      `category_id` int DEFAULT NULL, --商品分类id
      `status` tinyint DEFAULT NULL, --商品状态,0:未上架,1:上架
      `type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,--商品类型
      `shop_id` int DEFAULT NULL,  --店铺id
      `stock` int DEFAULT NULL,   --库存
      `rating` int DEFAULT NULL,  --浏览量
      `sold_count` int DEFAULT NULL,  --销量
      `review_count` int DEFAULT NULL,  --
      `price` decimal(10,2) DEFAULT NULL,  --价格
      `image` varchar(100) DEFAULT NULL,   --图片路径
      `create_at` datetime DEFAULT NULL,   --新增时间
      `updated_at` datetime DEFAULT NULL,   --修改时间
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
    

    订单表(order)

    CREATE TABLE `orders` (
      `id` bigint unsigned NOT NULL AUTO_INCREMENT,  --主键id
      `no` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, --订单编号
      `user_id` bigint unsigned NOT NULL,   --用户id
      `address` text COLLATE utf8mb4_unicode_ci NOT NULL,   --详细地址
      `total_amount` decimal(10,2) NOT NULL,   --订单总金额
      `remark` text COLLATE utf8mb4_unicode_ci,  --备注
      `paid_at` datetime DEFAULT NULL,  --支付时间
      `payment_method` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,  --支付类型
      `payment_no` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,  --支付编号
      `refund_status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',  --物流状态
      `refund_no` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, --物流编号
      `closed` tinyint(1) NOT NULL DEFAULT '0',  --订单关闭
      `reviewed` tinyint(1) NOT NULL DEFAULT '0', --订单评价
      `ship_status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', --
      `ship_data` text COLLATE utf8mb4_unicode_ci,
      `extra` text COLLATE utf8mb4_unicode_ci,
      `created_at` timestamp NULL DEFAULT NULL,
      `updated_at` timestamp NULL DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `orders_no_unique` (`no`),
      UNIQUE KEY `orders_refund_no_unique` (`refund_no`),
      KEY `orders_user_id_foreign` (`user_id`),
      CONSTRAINT `orders_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
    ) ENGINE=InnoDB AUTO_INCREMENT=420 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    

    订单详情表(order_items):

    CREATE TABLE `order_items` (
      `id` bigint unsigned NOT NULL AUTO_INCREMENT,
      `order_id` bigint unsigned NOT NULL,
      `product_id` bigint unsigned NOT NULL,
      `amount` int unsigned NOT NULL,
      `price` decimal(10,2) NOT NULL,
      `rating` int unsigned DEFAULT NULL,
      `review` text COLLATE utf8mb4_unicode_ci,
      `reviewed_at` timestamp NULL DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `order_items_order_id_foreign` (`order_id`),
      CONSTRAINT `order_items_order_id_foreign` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE
    ) ENGINE=InnoDB AUTO_INCREMENT=419 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    

    业务场景
    对于请求的数据进行验证以及查看商品库存:

    <?php
    namespace App\Http\Requests;
    
    use App\Models\Order;
    use App\Models\Product;
    use Illuminate\Foundation\Http\FormRequest;
    use Illuminate\Validation\Rule;
    
    class SeckillOrderRequest extends FormRequest
    {
        public function rules()
        {
            return [
                'product_id'     => [
                    'required',
                    function ($attribute, $value, $fail) {
                        if (!$product = Product::find($value)) {
                            return $fail('该商品不存在');
                        }
                        if ($product->type !== Product::TYPE_SECKILL) {
                            return $fail('该商品不支持秒杀');
                        }
                        if ($product->seckill->is_before_start) {
                            return $fail('秒杀尚未开始');
                        }
                        if ($product->seckill->is_after_end) {
                            return $fail('秒杀已经结束');
                        }
                        if (!$product->status) {
                            return $fail('该商品未上架');
                        }
                        if ($product->stock < 1) {
                            return $fail('该商品已售完');
                        }
    
                        if ($order = Order::query()
                            // 筛选出当前用户的订单
                            ->where('user_id', $this->post('userId'))
                            ->whereHas('items', function ($query) use ($value) {
                                // 筛选出包含当前 SKU 的订单
                                $query->where('product_id', $value);
                            })
                            ->where(function ($query) {
                                // 已支付的订单
                                $query->whereNotNull('paid_at')
                                    // 或者未关闭的订单
                                    ->orWhere('closed', false);
                            })
                            ->first()) {
                            if ($order->paid_at) {
                                return $fail('你已经抢购了该商品');
                            }
                            return $fail('你已经下单了该商品,请到订单页面支付');
                        }
                    },
                ],
            ];
        }
    }
    ?>
    

    对于活动下单时的处理:

    <?php
    namespace App\Services;
    
    use App\Models\Order;
    use App\Models\User;
    use App\Models\UserAddress;
    use App\Models\Product;
    use Carbon\Carbon;
    use App\Jobs\CloseOrder;
    
    class OrderService
    {
        public function seckill(User $user,UserAddress $address, Product $product)
        {
            $order = \DB::transaction(function () use ($user,$address, $product) {
                // 更新此地址的最后使用时间
                $address->update(['last_used_at' => Carbon::now()]);
                // 扣减对应 SKU 库存
                if ($product->decreaseStock(1) <= 0) {
                    throw new \Exception('该商品库存不足');
                }
                // 创建一个订单
                $order = new Order([
                    'address'      => [ // 将地址信息放入订单中
                        'address'       => $address->full_address,
                        'zip'           => $address->zip,
                        'contact_name'  => $address->contact_name,
                        'contact_phone' => $address->contact_phone,
                    ],
                    'remark'       => '',
                    'total_amount' => $product->price,
                    'type'         => Order::TYPE_SECKILL,
                    'paid_at'      =>  Carbon::now()
                ]);
                // 订单关联到当前用户
                $order->user()->associate($user);
                // 写入数据库
                $order->save();
                // 创建一个新的订单项并与 SKU 关联
                $item = $order->items()->make([
                    'amount' => 1, // 秒杀商品只能一份
                    'price'  => $product->price,
                ]);
                $item->product()->associate($product->id);
    //            $item->productSku()->associate($product);
                $item->save();
    
                return $order;
            });
            // 秒杀订单的自动关闭时间与普通订单不同
            dispatch(new CloseOrder($order, config('app.seckill_order_ttl')));
    
            if ($order){
                return ["status" => true,"message" => "下单成功,请到订单页面支付"];
            }else{
                return ["status" => false,"message" => "秒杀异常"];
            }
    
        }
    }
    ?>
    

    4.2 排行榜

    list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中,如京东每日的手机销量排行、学校每次月考学生的成绩排名、斗鱼年终盛典主播排名等。

    但是,并不是所有的排行榜都能用list类型实现,只有定时计算的排行榜才适合使用list类型存储,与定时计算的排行榜相对应的是实时计算的排行榜,list类型不能支持实时计算的排行榜,之后在介绍有序集合sorted set的应用场景时会详细介绍实时计算的排行榜的实现。

    相关文章

      网友评论

          本文标题:Redis-01数据类型之string类型与list类型

          本文链接:https://www.haomeiwen.com/subject/koxnoltx.html