Featured image of post redis应用(基础)

redis应用(基础)

本文阅读量

redis基础应用

验证码

1
2
3
4
5
6
# 添加验证码
setex uniqueId 30 "658428"
#验证验证码
get uniqueId  # 返回nil 不存在或过期
#销毁验证码
del uniqueId

计数器

场景

社交产品业务里有很多统计计数的功能,比如:

  • 用户: 总点赞数,关注数,粉丝数
  • 帖子: 点赞数,评论数,热度
  • 消息: 已读,未读,红点消息数
  • 话题: 阅读数,帖子数,收藏数

string实现

1
2
3
4
5
6
7
8
9
# 文章12的阅读数+1
incr article:readcount:{12}
# 获取文章12的阅读数
get article:readcount:{12}

# 批量设置阅读数
mset article:readcount:{12} old_val+1 article:readcount:{13} old_val+1 article:readcount:{14} old_val+1
# 批量获取阅读数
mget article:readcount:{12} article:readcount:{13} article:readcount:{14}

缺点:

  1. 会生成很多key,如:article:readcount:,article:him:
  2. 批量设置需要读取之前数据,在进行+1设置

hash实现

使用命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 给key值+1
hincrby hashkey key # hincrby user:1 followCnt

# 获取单个值
hget hashkey key # hget user:1 followCnt

# 获取多个值
hmget hashkey key1 key2

# 获取所有key
hgetall hashkey

设计方案1

1
2
3
4
5
6
# 用户
counter:user:{userID}
    ->  praiseCnt: 100      #点赞数
    ->  hostCnt: 200        #热度
    ->  followCnt: 332      #关注数
    ->  fansCnt: 123        #粉丝数

缺点

这样设计,如果要批量查询多个用户的数据,就比较麻烦,例如一次要查指定20个userID的计数器?只能循环执行 HGETALL counter:user:{userID}。

优点

以实体聚合数据,方便数据管理

优化:通过redis管道来优化,一次性发送多个命令给redis执行

1
2
3
4
5
6
7
8
9
$userIDArray = array(1001, 1002, 1003, 1009);

$pipe = $redis->multi(Redis::PIPELINE);
foreach ($userIDArray as $userID) {
    $pipe->hGetAll('counter:user:' . $userID);
}

$replies = $pipe->exec();
print_r($replies); 

设计方案2

1
2
3
4
5
6
# 用户点赞数
counter:user:praiseCnt
    ->  userID_1001: 100
    ->  userID_1002: 200
    .....
    ->  userID_9999: 123

优点

解决了批量操作的问题

缺点

当要获取多个计数器,比如同时需要praiseCnt,hostCnt时,要读多次,不过要比第一种方案读的次数要少。一个hash里的字段将会非常宠大,HMGET也许会有性能瓶颈

店铺筛选

应用场景

根据所在地,获取周围的店铺

geospetial实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 批量添加店铺经纬度
geoadd store:city1  121.47 31.23 store:1 106.50 29.53 store:2 115.05 22.52 store:3 120.15 30.28 store:4 125.14 42.92 store:5
# 获取(110,30)周围1km内的所有店铺名称
georadius store:city1 110 30 1 km
1) "store:1"
2) "store:2"
3) "store:3"

# 获取(110,30)周围1km内所有店铺经纬度列表
georadius store:city1 110 30 1 km withcoord
1) 1) "store:1"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"

标记状态

应用场景

给文章点赞,收藏

set实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 点赞
sadd like:消息id 用户id
# 取消点赞
srem like:消息id 用户id
# 检测用户是否点赞
sismember like:消息id 用户id
# 获取点赞的用户列表
smembrs like:消息id
# 获取点赞用户数
scard like:消息id

打卡

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 记录周一到周末的打卡
setbit sign 0 1 #周一打卡
setbit sign 1 0 #周二未打卡
setbit sign 2 1
setbit sign 3 1
setbit sign 4 1
setbit sign 5 0
setbit sign 6 1
getbit sign 6 # 查看星期天是否打卡
(integer) 1
bitcount sign #统计这周打卡天数,就可以看是否全勤
(integer) 5

https://blog.csdn.net/qq_39455116/article/details/87629029

统计uv

1
2
3
4
5
6
7
8
9
# 统计10.1号0:30 之间的uv
pfadd 10:1:0:30:uv uv1 uv2 uv3 uv4 uv5 uv6
# 统计10.1号0:30 - 1:00 之间的uv 
pfadd 10:1:1:00:uv uv2 uv4 uv5 uv7 uv9 uv10

# 统计10.1号uv数
pfmerge 10:1:all 10:1:0:30:uv 10:1:1:00:uv
pfcount 10:1:all
"9"

string

限速

限制每分钟请求次数

1
2
3
"user:$id:info:".$phonename;
exists
$Redis->incr($key)<=5

list

队列 秒杀

数据表

秒杀商品表(product_seckill):

1
2
3
4
5
6
7
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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)

 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
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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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

业务场景

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

 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
<?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('你已经下单了该商品,请到订单页面支付');
                    }
                },
            ],
        ];
    }
}
?>

对于活动下单时的处理:

 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
<?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" => "秒杀异常"];
        }

    }
}
?>

hash

购物车

优点:

  1. 不用操作数据库就可以实现功能;
  2. 用户可以不登录就将商品加入购物车中;

缺点:

  1. 用户关闭浏览器再访问没有记录;

    注:要给redis购物车信息加入过期时间;如果用户登录状态,可以用用户ID做标识符,就不需要给过期时间了

hash实现

使用命令

1
2
3
4
# 设置值
hset hashkey key val # hset cart:user:1 g1 1
# 获取单个值
hget hashkey key # hget user:1 followCnt

hashkey生成规则:

  • 如果用户登录 user:cart:{user_id}
  • 没有登录 user:cart:{session_id}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 添加/修改购物车
hget hashkey 1001 # 获取该商品1001添加到购物车数量
hset hashkey 1001 10 # 修改购物车中商品1001的数量为10

# 设置hashkey 过期时间
expire hashkey 1800


# 获取某个用户的购物车列表
hgetall hashkey


# 删除购物车
hdel hashkey 1001 # 删除购物车商品1001
hdel hashkey 1001 1002 # 批量删除购物车商品

zset

商品筛选/排行榜

场景

将商品按销量,评论数,价格区间筛选商品

商品的效率与评论数一般是需要统计的,即:商品表中没有该两字段的冗余字段,数据量大了,排序比价困难。

价格区间:mysql的range效率比较差,尤其是数据量庞大的时候

zset使用命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 赋值
zadd zkey sort key # zadd orderBySale 10 1001 #将商品1001的销售数量10存放到zset中

# 增加sort
zincrby zkey sort key # zincrby orderBySale 11 1001 # 将商品1001的销量增加到11

# 删除key
zrem zkey key1 key2 # zrem orderBySale 1001 1002 # 删除商品1001,1002

# 查询zkey成员数
zcard zkey  # zcard orderBySale # 有销量的商品数量,用于分页
# 按sort从小到大获取数
zrange zkey startkey endkey # zrange orderBySale 0 4 # 获取第[1,5]的销售数据
# 按sort从大到小获取数
zrevrange zkey startkey endkey # zrange orderBySale 5 9 # 获取第[6,10]的销售数据
# 正序
zrangebyscore zkey -inf sort limit startkey endKey 
# 倒序:
zrevrangebyscore zkey -inf sort limit startkey endKey 

zset实现销量与评论数排序,排行榜

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 批量添加所有商品的评论
zadd goodCommentSort 0 g1 0 g2 1 g3 3 g4 3 g5 10 g6

# 商品4 新增2条评论
zincrby goodCommentSort 2 g4

# 商品5 下架
zrem goodCommentSort g5

# 查询商品数量
zcard goodCommentSort

# 正序 查询商品第1页数据,每页30条数据
zrange goodCommentSort 0 29
# 倒序 查询商品第2页数据,每页30条数据
zrevrange goodCommentSort 30 59

zset实现价格区间排序

实现流程:

 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
# 批量添加所有商品的评论
zadd goodPriceSort 100 g1 120 g2 80 g3 95 g4 120 g5 160 g6

# 商品4 价格
zincrby goodPriceSort 15 g4

# 商品5 下架
zrem goodPriceSort g5

# 查询商品数量
zcard goodPriceSort

# 按区间查询
#获取小于100的记录,从小到大
ZRANGEBYSCORE goodPriceSort -inf 100 limit 0 3
# 按从小到大获取价格(100,200]记录
zrangebyscore goodPriceSort (100 200 limit 0 3
# 按从小到大获取价格(100,200)记录
zrangebyscore goodPriceSort (100 (200 limit 0 3

# 获取大于100的记录,从大到小
zrevrangebyscore goodPriceSort +inf 100 limit 0 4
# 按从大到小获取价格[100,200]记录
zrevrangebyscore goodPriceSort 200 100 limit 0 4 
# 按从大到小获取价格小于等于100记录
zrevrangebyscore goodPriceSort 100 -inf limit 0 4

list实现

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

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

使用 Hugo 构建
主题 StackJimmy 设计