一、环境准备
1. 初始化项目 & 安装依赖
1 2 3 4
| npm init -y
npm install koa koa-router ioredis
|
2. 基础配置(Redis连接 + Koa启动)
创建 app.js 作为入口文件,先完成Redis客户端初始化和Koa基础配置:
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
| const Koa = require('koa'); const Router = require('koa-router'); const Redis = require('ioredis');
const app = new Koa(); const router = new Router();
const redis = new Redis({ host: '127.0.0.1', port: 6379, password: '', db: 0, retryStrategy: (times) => { const delay = Math.min(times * 100, 3000); return delay; } });
redis.on('error', (err) => { console.error('Redis连接失败:', err); });
app.use(router.routes()).use(router.allowedMethods()); app.listen(3000, () => { console.log('Koa服务启动:http://localhost:3000'); });
module.exports = { redis };
|
二、各数据结构实战(附Koa路由代码)
以下所有示例均基于上述基础配置,在 app.js 中追加路由即可。
1. String(字符串)
核心场景:缓存简单数据、计数器(阅读量/点赞数)、分布式锁
核心API:set/get/incr/expire/setnx
示例1:缓存用户信息(序列化JSON)
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
|
router.get('/user/info/:userId', async (ctx) => { const { userId } = ctx.params; const cacheKey = `user:info:${userId}`;
const cacheData = await redis.get(cacheKey); if (cacheData) { ctx.body = { code: 200, msg: '从缓存获取', data: JSON.parse(cacheData) }; return; }
const dbData = { userId, username: `用户${userId}`, age: 20 + parseInt(userId), phone: `1380000${userId.padStart(4, '0')}` };
await redis.set(cacheKey, JSON.stringify(dbData), 'EX', 3600);
ctx.body = { code: 200, msg: '从数据库获取', data: dbData }; });
|
示例2:文章阅读量计数器
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
|
router.get('/article/visit/:articleId', async (ctx) => { const { articleId } = ctx.params; const countKey = `article:visit:${articleId}`;
const newCount = await redis.incr(countKey);
ctx.body = { code: 200, msg: '阅读量+1成功', data: { articleId, visitCount: newCount } }; });
router.get('/article/visit/count/:articleId', async (ctx) => { const { articleId } = ctx.params; const countKey = `article:visit:${articleId}`;
const visitCount = await redis.get(countKey) || 0;
ctx.body = { code: 200, data: { articleId, visitCount: parseInt(visitCount) } }; });
|
2. Hash(哈希)
核心场景:存储多字段对象(用户详情)、购物车(用户ID→商品ID:数量)
核心API:hset/hget/hgetall/hincrby/hdel
示例1:存储用户详情(多字段,无需序列化JSON)
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
|
router.post('/user/save/:userId', async (ctx) => { const { userId } = ctx.params; const { username, age, email } = ctx.request.body; const hashKey = `user:hash:${userId}`;
await redis.hset(hashKey, { username, age, email, updateTime: new Date().toISOString() });
await redis.expire(hashKey, 86400);
ctx.body = { code: 200, msg: '用户信息保存成功' }; });
router.get('/user/hash/:userId', async (ctx) => { const { userId } = ctx.params; const hashKey = `user:hash:${userId}`;
const userInfo = await redis.hgetall(hashKey);
ctx.body = { code: 200, data: userInfo || {} }; });
|
示例2:购物车功能
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
|
router.post('/cart/add', async (ctx) => { const { userId, productId, count } = ctx.request.body; const cartKey = `cart:${userId}`;
await redis.hincrby(cartKey, productId, count);
ctx.body = { code: 200, msg: '添加购物车成功' }; });
router.get('/cart/list/:userId', async (ctx) => { const { userId } = ctx.params; const cartKey = `cart:${userId}`;
const cartList = await redis.hgetall(cartKey); const formatCart = Object.entries(cartList).map(([productId, count]) => ({ productId, count: parseInt(count) }));
ctx.body = { code: 200, data: formatCart }; });
router.post('/cart/delete', async (ctx) => { const { userId, productId } = ctx.request.body; const cartKey = `cart:${userId}`;
await redis.hdel(cartKey, productId);
ctx.body = { code: 200, msg: '删除商品成功' }; });
|
3. List(列表)
核心场景:简单消息队列、最新动态列表(朋友圈/评论)
核心API:lpush/rpush/lpop/rpop/lrange/ltrim
示例1:简单消息队列(生产者+消费者)
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
| const msgQueueKey = 'queue:msg';
router.post('/queue/produce', async (ctx) => { const { content } = ctx.request.body; const msg = { id: Date.now() + Math.random().toString(36).slice(2), content, time: new Date().toISOString() };
await redis.lpush(msgQueueKey, JSON.stringify(msg));
ctx.body = { code: 200, msg: '消息生产成功', data: msg.id }; });
router.get('/queue/consume', async (ctx) => { const msgStr = await redis.rpop(msgQueueKey); if (!msgStr) { ctx.body = { code: 200, msg: '队列无消息' }; return; }
const msg = JSON.parse(msgStr); ctx.body = { code: 200, msg: '消费消息成功', data: msg }; });
|
示例2:最新动态列表(限制仅保留10条)
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
|
router.post('/dynamic/add/:userId', async (ctx) => { const { userId } = ctx.params; const { content } = ctx.request.body; const dynamicKey = `dynamic:${userId}`; const dynamic = { content, time: new Date().toISOString() };
await redis.lpush(dynamicKey, JSON.stringify(dynamic)); await redis.ltrim(dynamicKey, 0, 9);
ctx.body = { code: 200, msg: '动态添加成功' }; });
router.get('/dynamic/list/:userId', async (ctx) => { const { userId } = ctx.params; const dynamicKey = `dynamic:${userId}`;
const dynamicList = await redis.lrange(dynamicKey, 0, -1); const formatList = dynamicList.map(item => JSON.parse(item));
ctx.body = { code: 200, data: formatList }; });
|
4. Set(集合)
核心场景:好友关系(共同好友)、抽奖、标签去重
核心API:sadd/smembers/sinter/srandmember/spop
示例1:共同好友查询
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
|
router.post('/friend/add', async (ctx) => { const { userId, friendId } = ctx.request.body; const friendKey = `friend:${userId}`;
await redis.sadd(friendKey, friendId);
ctx.body = { code: 200, msg: '添加好友成功' }; });
router.get('/friend/common/:userId1/:userId2', async (ctx) => { const { userId1, userId2 } = ctx.params; const key1 = `friend:${userId1}`; const key2 = `friend:${userId2}`;
const commonFriends = await redis.sinter(key1, key2);
ctx.body = { code: 200, data: { userId1, userId2, commonFriends } }; });
|
示例2:抽奖功能
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
| const lotteryKey = 'lottery:pool';
router.post('/lottery/join', async (ctx) => { const { userId } = ctx.request.body;
await redis.sadd(lotteryKey, userId);
ctx.body = { code: 200, msg: '加入抽奖成功' }; });
router.get('/lottery/draw/:count', async (ctx) => { const { count } = ctx.params; const drawCount = parseInt(count);
const winners = await redis.spop(lotteryKey, drawCount);
ctx.body = { code: 200, msg: `抽中${drawCount}人`, data: { winners } }; });
|
5. Sorted Set(有序集合)
核心场景:排行榜、延时队列
核心API:zadd/zrevrange/zrangebyscore/zrem
示例1:游戏积分排行榜
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
| const rankKey = 'rank:game:score';
router.post('/rank/add', async (ctx) => { const { userId, score } = ctx.request.body;
await redis.zadd(rankKey, score, userId);
ctx.body = { code: 200, msg: '积分添加成功' }; });
router.get('/rank/top/:size', async (ctx) => { const { size } = ctx.params; const topSize = parseInt(size);
const rankList = await redis.zrevrange(rankKey, 0, topSize - 1, 'WITHSCORES'); const formatRank = []; for (let i = 0; i < rankList.length; i += 2) { formatRank.push({ rank: i / 2 + 1, userId: rankList[i], score: parseInt(rankList[i + 1]) }); }
ctx.body = { code: 200, data: formatRank }; });
|
示例2:延时队列(按时间戳执行任务)
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
| const delayQueueKey = 'delay:queue';
router.post('/delay/add', async (ctx) => { const { taskId, delaySeconds, content } = ctx.request.body; const executeTime = Date.now() + delaySeconds * 1000;
const task = JSON.stringify({ taskId, content }); await redis.zadd(delayQueueKey, executeTime, task);
ctx.body = { code: 200, msg: '延时任务添加成功', data: { executeTime: new Date(executeTime).toISOString() } }; });
router.get('/delay/consume', async (ctx) => { const now = Date.now();
const tasks = await redis.zrangebyscore(delayQueueKey, 0, now, 'LIMIT', 0, 1); if (tasks.length === 0) { ctx.body = { code: 200, msg: '暂无到期任务' }; return; }
const task = JSON.parse(tasks[0]); await redis.zrem(delayQueueKey, tasks[0]);
ctx.body = { code: 200, msg: '消费延时任务成功', data: task }; });
|
6. BitMap(位图)
核心场景:用户签到、在线状态
核心API:setbit/getbit/bitcount
示例1:用户签到功能
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
|
router.post('/sign/in/:userId/:day', async (ctx) => { const { userId, day } = ctx.params; const signKey = `sign:${userId}:202506`; const dayNum = parseInt(day) - 1;
await redis.setbit(signKey, dayNum, 1);
ctx.body = { code: 200, msg: '签到成功' }; });
router.get('/sign/count/:userId', async (ctx) => { const { userId } = ctx.params; const signKey = `sign:${userId}:202506`;
const signCount = await redis.bitcount(signKey);
ctx.body = { code: 200, data: { userId, signCount, month: '2025年06月' } }; });
|
示例2:用户在线状态
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
|
router.post('/status/set/:userId/:status', async (ctx) => { const { userId, status } = ctx.params; const statusKey = 'user:online:status'; const userIdNum = parseInt(userId);
await redis.setbit(statusKey, userIdNum, status);
ctx.body = { code: 200, msg: `用户${userId}状态设为${status === '1' ? '在线' : '离线'}` }; });
router.get('/status/get/:userId', async (ctx) => { const { userId } = ctx.params; const statusKey = 'user:online:status'; const userIdNum = parseInt(userId);
const status = await redis.getbit(statusKey, userIdNum);
ctx.body = { code: 200, data: { userId, status: status === 1 ? '在线' : '离线' } }; });
|
7. HyperLogLog(基数统计)
核心场景:UV统计(无需存储用户ID,仅统计去重人数)
核心API:pfadd/pfcount
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
| const uvKey = 'uv:website:202506';
router.post('/uv/add/:userId', async (ctx) => { const { userId } = ctx.params;
await redis.pfadd(uvKey, userId);
ctx.body = { code: 200, msg: '记录访问用户成功' }; });
router.get('/uv/count', async (ctx) => { const uvCount = await redis.pfcount(uvKey);
ctx.body = { code: 200, data: { month: '2025年06月', uvCount: parseInt(uvCount) } }; });
|
8. Geo(地理位置)
核心场景:附近的商家/附近的人
核心API:geoadd/georadius/geodist
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
| const geoKey = 'geo:merchant';
router.post('/geo/add', async (ctx) => { const { merchantId, lng, lat, name } = ctx.request.body; await redis.geoadd(geoKey, lng, lat, `${merchantId}:${name}`);
ctx.body = { code: 200, msg: '商家位置添加成功' }; });
router.get('/geo/near/:lng/:lat/:radius', async (ctx) => { const { lng, lat, radius } = ctx.params; const radiusNum = parseInt(radius);
const nearMerchants = await redis.georadius( geoKey, lng, lat, radiusNum, 'm', 'WITHDIST', 'WITHCOORD', 'ASC' );
const formatMerchants = nearMerchants.map(item => { const [idName, dist, [lng, lat]] = item; const [merchantId, name] = idName.split(':'); return { merchantId, name, distance: `${parseFloat(dist).toFixed(2)}米`, location: { lng, lat } }; });
ctx.body = { code: 200, data: formatMerchants }; });
|
三、补充说明
- 请求体解析:上述POST接口需配合
koa-body 解析JSON请求体,安装后在 app.js 中添加:1 2
| const { koaBody } = require('koa-body'); app.use(koaBody());
|
- Redis异常处理:生产环境需给每个Redis操作加try/catch,避免单个接口失败导致服务崩溃;
- 性能优化:批量操作建议用
pipeline,例如批量添加商家位置:1 2 3 4
| const pipeline = redis.pipeline(); pipeline.geoadd(geoKey, 116.40, 39.91, 'm001:北京店'); pipeline.geoadd(geoKey, 116.41, 39.92, 'm002:北京西单店'); await pipeline.exec();
|