Skip to content

GlobalModule 全局模块使用文档

概述

GlobalModule 是一个 NestJS 全局模块,使用 @Global() 装饰器标记,使得模块中导出的服务可以在整个应用程序中直接使用,无需在其他模块中重复导入。该模块主要提供 Redis 缓存服务和一些全局共享的功能模块。

模块结构

src/modules/global/
├── global.module.ts    # 全局模块定义
└── redis.service.ts    # Redis 服务实现

基础配置

模块定义

typescript
import { Global, Module } from '@nestjs/common'
import { RedisService } from './redis.service'
import { LoginLogsModule } from '../loginLogs/module'
import { MenusModule } from '../menus/menus.module'

@Global()
@Module({
  imports: [LoginLogsModule, MenusModule],
  controllers: [],
  providers: [RedisService],
  exports: [RedisService],
})
export class GlobalModule {}

关键特性:

  • 使用 @Global() 装饰器标记为全局模块
  • 导入 LoginLogsModuleMenusModule 提供相关功能
  • 导出 RedisService 供整个应用使用

在 AppModule 中引入

typescript
import { Module } from '@nestjs/common'
import { GlobalModule } from './modules/global/global.module'

@Module({
  imports: [
    GlobalModule,
    // ... 其他模块
  ],
})
export class AppModule {}

RedisService API 参考

初始化配置

Redis 服务在构造函数中自动初始化连接:

typescript
constructor(
  private loginLogsService: LoginLogsService,
  private menusService: MenusService,
) {
  this.redis = new Redis({
    port: 6379,        // Redis 端口
    host: '127.0.0.1', // Redis 主机地址
    db: 1,             // 数据库索引
  })
}

注意: 生产环境建议将 Redis 配置提取到配置文件中进行管理。

基础操作方法

set(key: string, value: any, time?: number)

设置键值对,支持过期时间。

参数:

  • key: 字符串类型的键
  • value: 任意类型的值(对象会自动序列化为 JSON)
  • time: 可选,过期时间(秒)

返回: Promise<any>

typescript
// 基本使用
await redisService.set('username', 'admin')

// 带过期时间(5分钟)
await redisService.set('session:token', tokenData, 300)

// 存储对象
await redisService.set('user:1', { id: 1, name: 'Admin' })

get(key: string)

获取指定键的值。

参数:

  • key: 字符串类型的键

返回: Promise<string | null> - 如果值是 JSON 字符串,需要手动解析

typescript
const username = await redisService.get('username')
const userData = JSON.parse(await redisService.get('user:1'))

del(key: string)

删除指定键。

参数:

  • key: 字符串类型的键

返回: Promise<number> - 删除的键数量

typescript
await redisService.del('username')

ttl(key: string)

获取键的剩余生存时间。

参数:

  • key: 字符串类型的键

返回: Promise<number> - 剩余秒数,-1 表示永不过期,-2 表示键不存在

typescript
const remainingTime = await redisService.ttl('session:token')

keys(pattern: string)

查找匹配模式的键。

参数:

  • pattern: 匹配模式(支持通配符 *

返回: Promise<string[]>

typescript
// 获取所有用户在线键
const onlineUsers = await redisService.keys('user.online:*')

expire(key: string, time: number)

设置键的过期时间。

参数:

  • key: 字符串类型的键
  • time: 过期时间(秒)

返回: Promise<number>

typescript
// 延长会话时间到 10 分钟
await redisService.expire('session:token', 600)

exists(key: string)

检查键是否存在。

参数:

  • key: 字符串类型的键

返回: Promise<number> - 1 表示存在,0 表示不存在

typescript
const isExists = await redisService.exists('username')
if (isExists) {
  console.log('键存在')
}

Hash 操作方法

hset(key: string, field: string, value: string)

设置哈希表中字段的值。

参数:

  • key: 哈希表的键
  • field: 字段名
  • value: 字段值

返回: Promise<number>

typescript
await redisService.hset('user:1', 'name', 'Admin')
await redisService.hset('user:1', 'email', 'admin@example.com')

hget(key: string, field: string)

获取哈希表中指定字段的值。

参数:

  • key: 哈希表的键
  • field: 字段名

返回: Promise<string | null>

typescript
const name = await redisService.hget('user:1', 'name')

hdel(key: string, field: string)

删除哈希表中指定的字段。

参数:

  • key: 哈希表的键
  • field: 字段名

返回: Promise<number>

typescript
await redisService.hdel('user:1', 'email')

高级查询方法

getNotExpiredValues(pattern = '*')

获取所有未过期的键值对。

参数:

  • pattern: 可选,匹配模式,默认为 '*'

返回: Promise<any[]> - 解析后的对象数组

typescript
// 获取所有未过期的用户会话
const activeSessions = await redisService.getNotExpiredValues('user.online:*')

// 遍历处理
activeSessions.forEach(session => {
  console.log(session.account, session.ip)
})

实现原理:

  • 使用 SCAN 命令分批遍历键,避免阻塞 Redis
  • 检查每个键的 TTL,只返回未过期的键
  • 自动解析 JSON 字符串为对象

用户会话管理方法

getRedisOnlineUser(query: QueryListDto = {})

获取在线用户列表,支持分页和筛选。

参数:

  • query: 查询条件对象
    • pageNum: 页码(从 1 开始)
    • pageSize: 每页数量
    • createTimeRange: 时间范围 [startTime, endTime]
    • account: 账号模糊搜索
    • ip: IP 地址模糊搜索
    • address: 地址模糊搜索

返回: Promise<[any[], number]> - [数据数组, 总数]

typescript
// 基本查询
const [users, total] = await redisService.getRedisOnlineUser({
  pageNum: 1,
  pageSize: 10,
})

// 带筛选条件
const [filteredUsers, total] = await redisService.getRedisOnlineUser({
  pageNum: 1,
  pageSize: 10,
  account: 'admin',
  ip: '192.168',
  createTimeRange: ['2024-01-01', '2024-12-31'],
})

setRedisOnlineUser(reqOrData, user: any = {})

设置用户在线状态。

参数:

  • reqOrData: 请求对象或数据对象
  • user: 可选,用户信息对象

返回: Promise<any>

typescript
// 方式一:传入包含 session 的数据
await redisService.setRedisOnlineUser({
  session: 'abc123',
  account: 'admin',
  ip: '192.168.1.1',
  address: '北京',
  createTime: new Date(),
})

// 方式二:传入请求对象和用户信息
await redisService.setRedisOnlineUser(request, userInfo)
// 会自动创建登录日志并存储

功能说明:

  • 如果传入的数据包含 session,直接存储该数据
  • 否则,调用 LoginLogsService 创建登录日志后存储
  • 默认过期时间为 5 分钟(300 秒)

delRedisOnlineUser(session: string)

删除用户在线状态。

参数:

  • session: 用户会话 ID

返回: Promise<number>

typescript
// 用户登出时调用
await redisService.delRedisOnlineUser(userSession)

权限管理方法

getPermissions(): Promise<string[]>

获取系统所有按钮权限标识。

返回: Promise<string[]> - 权限标识数组

typescript
// 获取所有权限
const permissions = await redisService.getPermissions()
console.log(permissions) 
// ['user:add', 'user:update', 'user:delete', ...]

功能说明:

  • 首先尝试从 Redis 缓存中读取权限数据
  • 如果缓存不存在,从数据库查询所有激活的按钮类型菜单
  • 提取菜单的 permissionKey 字段
  • 将结果缓存到 Redis,键名为 permissions
  • 后续请求直接从缓存读取,提高性能

使用场景:

typescript
// 在守卫中使用
@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private redisService: RedisService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const permissions = await this.redisService.getPermissions()
    // 验证用户是否有对应权限
    return permissions.includes(requiredPermission)
  }
}

实际应用场景

1. 用户登录会话管理

typescript
import { Injectable } from '@nestjs/common'
import { RedisService } from '../global/redis.service'

@Injectable()
export class AuthService {
  constructor(private redisService: RedisService) {}

  async login(username: string, password: string) {
    // 验证用户...
    const user = await this.validateUser(username, password)
    
    // 生成会话 ID
    const session = this.generateSession()
    
    // 记录在线状态
    await this.redisService.setRedisOnlineUser({
      session,
      account: user.username,
      ip: this.getRequestIp(),
      address: await this.getIpAddress(),
      createTime: new Date(),
    }, user)
    
    return { session, user }
  }

  async logout(session: string) {
    // 删除在线状态
    await this.redisService.delRedisOnlineUser(session)
  }
}

2. 接口访问频率限制

typescript
import { Injectable } from '@nestjs/common'
import { RedisService } from '../global/redis.service'

@Injectable()
export class RateLimitService {
  constructor(private redisService: RedisService) {}

  async checkRateLimit(userId: string, limit = 100, windowSeconds = 3600) {
    const key = `rate.limit:${userId}`
    const count = await this.redisService.get(key)
    
    if (!count) {
      // 首次访问,设置计数器和过期时间
      await this.redisService.set(key, 1, windowSeconds)
      return true
    }
    
    const currentCount = parseInt(count)
    if (currentCount >= limit) {
      return false // 超出限制
    }
    
    // 增加计数
    await this.redisService.set(key, currentCount + 1)
    return true
  }
}

3. 数据缓存优化

typescript
import { Injectable } from '@nestjs/common'
import { RedisService } from '../global/redis.service'
import { UserService } from '../users/user.service'

@Injectable()
export class CacheService {
  constructor(
    private redisService: RedisService,
    private userService: UserService,
  ) {}

  async getUserWithCache(userId: number) {
    const cacheKey = `user:${userId}`
    
    // 尝试从缓存获取
    let user = await this.redisService.get(cacheKey)
    
    if (!user) {
      // 缓存未命中,查询数据库
      user = await this.userService.findById(userId)
      
      // 存入缓存,过期时间 10 分钟
      await this.redisService.set(cacheKey, user, 600)
    } else {
      user = JSON.parse(user)
    }
    
    return user
  }

  async clearUserCache(userId: number) {
    await this.redisService.del(`user:${userId}`)
  }
}

4. 实时在线用户统计

typescript
import { Controller, Get, Query } from '@nestjs/common'
import { RedisService } from '../global/redis.service'

@Controller('monitor')
export class MonitorController {
  constructor(private redisService: RedisService) {}

  @Get('online-users')
  async getOnlineUsers(@Query() query) {
    const [users, total] = await this.redisService.getRedisOnlineUser({
      pageNum: query.pageNum || 1,
      pageSize: query.pageSize || 20,
      account: query.account,
      ip: query.ip,
    })

    return {
      code: 200,
      data: {
        list: users,
        total,
      },
    }
  }
}

注意事项

1. Redis 连接配置

当前 Redis 配置是硬编码在代码中的,生产环境建议:

typescript
// 推荐:从配置文件读取
import { ConfigService } from '@nestjs/config'

constructor(private configService: ConfigService) {
  this.redis = new Redis({
    port: this.configService.get('REDIS_PORT') || 6379,
    host: this.configService.get('REDIS_HOST') || '127.0.0.1',
    password: this.configService.get('REDIS_PASSWORD'),
    db: this.configService.get('REDIS_DB') || 0,
  })
}

2. 键命名规范

建议使用统一的键命名规范,避免冲突:

  • 用户会话:user.online:{session}
  • 用户信息缓存:user:{id}
  • 权限缓存:permissions
  • 速率限制:rate.limit:{userId}
  • 业务数据:{module}:{type}:{id}

3. 过期时间管理

  • 会话数据:建议 5-30 分钟
  • 用户信息缓存:建议 10-60 分钟
  • 权限数据:建议较长(如 24 小时)或手动更新
  • 临时数据:根据业务需求设置

4. 性能优化

  • 使用 SCAN 而非 KEYS 命令遍历键(已实现)
  • 批量操作时使用 pipeline
  • 合理设置过期时间,避免内存泄漏
  • 监控 Redis 内存使用情况

5. 错误处理

建议在调用 Redis 方法时添加错误处理:

typescript
try {
  await this.redisService.set('key', 'value')
} catch (error) {
  console.error('Redis 操作失败:', error)
  // 降级处理逻辑
}

依赖模块

LoginLogsModule

用于记录和管理用户登录日志,在 setRedisOnlineUser 方法中被调用。

用于管理系统菜单和权限,在 getPermissions 方法中被调用。

总结

GlobalModule 通过提供全局可用的 RedisService,为应用程序提供了强大的缓存和会话管理能力。合理使用 Redis 可以显著提升系统性能和用户体验,特别是在高并发场景下。

核心优势:

  • ✅ 全局可用,无需重复导入
  • ✅ 封装常用 Redis 操作,简化开发
  • ✅ 内置用户会话管理功能
  • ✅ 支持权限缓存,提升性能
  • ✅ 提供灵活的查询和筛选能力

最佳实践:

  • 合理设计缓存策略和过期时间
  • 使用统一的键命名规范
  • 做好错误处理和降级方案
  • 定期监控和优化 Redis 性能