# 扩展库Redis

2021年11月18日,腾讯云和阿里云均支持

使用腾讯云node12和redis,务必仔细阅读此文档:keepRunningAfterReturn

Redis是一个基于key/value的内存数据库。在项目中通常作为MongoDB等磁盘数据库的补充来搭配使用。 相对于磁盘数据库,Redis的核心优势是快。因为操作内存要比磁盘快的多,并且Redis只支持key/value数据,读写都很快。但Redis没有磁盘数据库丰富的查询等功能。

Redis一般需要与MongoDB搭配使用,MongoDB做主存储,Redis缓存部分数据应对高性能需求场景。

在uniCloud中,Redis还有一个特点,是它按容量和使用时长计费,访问它不耗费云数据库的读写次数。

Redis常见使用场景:

  • 缓存高频数据,比如首页列表、banner列表、热门排行。MongoDB数据更新后同步给Redis,这些频繁的请求就不再查询MongoDB数据库
  • 秒杀、抢购。短时间大量并发可能发生超卖,此时必须使用Redis解决
  • ip黑名单屏蔽。希望较快封杀某些ip请求,缓解MongoDB数据库压力。
  • 其他数据库操作速度不满足需求的场景

# 开通Redis扩展库

参考开通redis

# 为云函数启用redis扩展库

Redis的sdk体积不小,没有内置在云函数中。在需要Redis的云函数里,开发者需手动配置Redis扩展库。(Redis没有免费试用,需购买才可以使用)

  • HBuilderX 3.4起提供了可视化界面,新建云函数/云对象时可选择Redis扩展库,或者在已有的云函数目录点右键选择“管理公共模块或扩展库依赖”

  • HBuilderX 3.4以前,没有可视化界面,需要开发者手动在云函数/云对象的package.json内添加云函数的扩展库(如果云函数目录下没有package.json,可以通过在云函数目录下执行npm init -y来生成)

下面是一个开启了redis扩展库的云函数的package.json示例,注意不可有注释,以下文件内容中的注释仅为说明,如果拷贝此文件,切记去除注释

{
	"name": "redis-test",
	"version": "1.0.0",
	"description": "",
	"main": "index.js",
	"extensions": {
		"uni-cloud-redis": {} // 配置为此云函数开启redis扩展库,值为空对象留作后续追加参数,暂无内容
	},
	"author": ""
}

# 云函数中调用Redis

// 云函数中调用Redis示例
'use strict';
const redis = uniCloud.redis()
exports.main = async (event, context) => {
	const getResult = await redis.get('my-key')
	const setResult = await redis.set('my-key', 'value-test')
	return {
    getResult,
    setResult
  }
};

注意

  • redis中,以冒号分割key,在redis的uniCloud web控制台的可视化界面中,将以tree的方式显示。折叠所有使用同一前缀的key。 比如2个key,uni:aauni:bb,将显示为根节点为uni的tree,展开后有aa和bb。
  • uni:dcloud:unicloud:为前缀的redis的key,为官方前缀。开发者自己的业务所需的key应避免使用这些前缀。
  • 调用uniCloud.redis()时返回的redis实例对应着一个连接,多次调用时如果存在未断开连接的redis实例则返回此实例。如果不存在redis实例或之前的redis实例已断开连接则返回新的redis实例。
  • redis实例创建时并未建立与redis的连接,而是在第一次调用redis方法时才会与redis建立连接。在实际业务中的表现就是一个云函数实例第一次调用redis方法会慢上几毫秒
  • 为云函数开启redis扩展会影响云函数固定ip功能,详情参考:云函数固定出口IP

# Redis本地运行

HBuilderX 3.4.10 起支持

因为Redis在云函数的内网中,所以只能在云端云函数中访问,而不能在本地云函数中访问。每次调试Redis相关功能需要不断的上传云函数,严重影响开发效率。自HBuilderX 3.4.10起,本地云函数可以通过云端内置代理访问云端Redis。如果在本地调用云端Redis的话会在云函数日志内看到HBuilderX运行调试Redis的代理请求字样。

# 数据类型

Redis中数据被存储为key-value形式,key均为字符串,value有以下几种类型

# 字符串String

字符串类型,这是最简单Redis类型。需要注意的是Redis并没有number类型,如果存入number类型的数据最终也会转为string类型。

await redis.set('string-key', 1) // 设置string-key的值为字符串"1"
await redis.get('string-key') // 获取string-key的值,"1"

# 列表List

列表类型,类似JavaScript中的数组,但是有区别。严格来说List是基于链表实现的,和js中数组相比一个显著的差异就是头部插入的效率。如果你测试过往一个长度百万的数组最前面插入一位的话,你会发现这个操作会耗时很久。但是List并没有这个问题,对于List来说在前后插入数据耗时是一样的。

注意

  • list为空时对应的键会被删除,即redis内不存在空List
await redis.lpush('list-key', 1) // 往list-key左侧添加一个元素,不存在则创建

# 散列Hash

Hash类型类似js里面的Object。

await redis.hmset('hash-key', 'key1', 'value1', 'key2', 'value2') // 批量为hash-key添加键值,不存在则创建
await redis.hset('hash-key', 'key1', 'value1') // 为hash-key添加键值,不存在则创建

# 集合Set

集合是String的无序排列,集合内的元素不可重复

await redis.sadd('set-key', 'value1', 'value2') // 往集合内添加数据,不存在则创建

# 有序集合Sorted Sets

有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素将有一个double类型的分数(分数不一定是连续的),用于对元素进行排序

await redis.zadd('sorted-set-key', 1, 'value1') // 往有序集合内添加数据并指定分数,不存在则创建
await redis.zadd('sorted-set-key', 2, 'value2')

# API

此处仅列举常见命令,完整命令支持请查看redis官方文档 (opens new window)

# get

用于获取字符串类型的数据

接口形式

await redis.get(key: string)

入参说明

参数 说明 必填 说明
key

返回值

此接口返回获取到的数据(字符串类型),返回null表示无此键

示例

await redis.get('string-key') // '1'

# set

用于设置字符串类型数据,新增、修改均可

接口形式

该接口有多种形式

await redis.set(key: string, value: string, flag: string)
await redis.set(key: string, value: string, mode: string, duration: number)
await redis.set(key: string, value: string, mode: string, duration: number, flag: string)

入参说明

参数 说明 必填 说明
key
value
duration 过期时间,到期后自动删除
mode 标识duration的单位 否(duration不为空时必填) EX:单位秒,PX:单位毫秒
flag 区分状态进行SET NX:不存在时才设置,XX:存在时才设置

返回值

此接口返回字符串类型'OK'表示操作成功,返回null表示未更新

示例

await redis.set('string-key', 1)  // redis内存储为字符串"1"
await redis.set('string-key', '1', 'NX')  // string-key不存在时设置为1
await redis.set('string-key', '1', 'EX', 100)  // string-key 100秒后过期
await redis.set('string-key', '1', 'EX', 100, 'NX')  // string-key不存在时设置为1,过期时间设置为100秒

# setex

键存在时,设置为指定字符串并指定过期时间

接口形式

await redis.setex(key: string, seconds: number, value: string)

入参说明

参数 说明 必填 说明
key
seconds 过期时间 单位:秒
value

返回值

此接口返回字符串类型'OK'表示操作成功,返回null表示未更新

示例

await redis.setex('string-key', 10, 'value')  // 值设置为value,过期时间10秒

# setnx

键不存在时,设置为指定字符串

接口形式

await redis.setnx(key: string, value: string)

入参说明

参数 说明 必填 说明
key
value

返回值

此接口返回字符串类型'OK'表示操作成功,返回null表示未更新

示例

await redis.setnx('string-key', 'value')  // 键string-key不存在时将值设置为'value'

# mget

批量获取指定键的值

接口形式

await redis.mget(key1: string, key2: string, ...)

入参说明

接收一个键的列表

返回值

此接口按传入顺序返回获取到的数据组成的数组,存在的键返回字符串类型,不存在的键返回null

示例

await redis.mget('key1', 'key2') // '1'

# mset

批量设置键值

接口形式

await redis.mset(key1: string, value1: string, key2: string, value2: string, ...)

入参说明

接收一个键、值的列表

返回值

此接口只会返回OK

示例

await redis.mset('key1', '1', 'key2', '2') // 'OK'

# del

用于删除执行的键

接口形式

await redis.del(key: string)

入参说明

参数 说明 必填 说明
key

返回值

接口返回数字1表示删除成功,数字0表示键不存在删除失败

示例

await redis.del('string-key') // '1'

# incr

对指定的键执行加1操作

接口形式

await redis.incr(key: string)

入参说明

参数 说明 必填 说明
key

返回值

接口返回执行加一操作后的值(number类型)

注意

操作的值并非整数形式(例:字符串"1"是整数形式,字符串"a"非整数形式)时会直接抛出错误

示例

await redis.set('string-key', '1')
await redis.incr('string-key') // 2

# incrby

在指定的键上加一个整数

接口形式

await redis.incrby(key: string, increment: number)

入参说明

参数 说明 必填 说明
key
increment 增加的值

返回值

接口返回执行加操作后的值(number类型)

注意

操作的值并非整数形式(例:字符串"1"是整数形式,字符串"a"非整数形式)时会直接抛出错误

示例

await redis.set('string-key', '1')
await redis.incrby('string-key', 2) // 3

# incrbyfloat

在指定的键上加一个浮点数

接口形式

await redis.incrbyfloat(key: string, increment: number)

入参说明

参数 说明 必填 说明
key
increment 增加的值,允许为负值来实现相减功能

返回值

接口返回执行加操作后的值(number类型)

注意

  • 操作的值并非整数形式(例:字符串"1"是整数形式,字符串"a"非整数形式)时会直接抛出错误
  • 浮点数相加和js内表现一致,可能与预期结果不一致,见下方示例

示例

await redis.set('string-key', '1.1')
await redis.incrbyfloat('string-key', 2.2) // 3.30000000000000027
// js内执行 0.1 + 0.2 会得到类似的值 3.3000000000000003

# decr

对指定的键执行减1操作

接口形式

await redis.decr(key: string)

入参说明

参数 说明 必填 说明
key

返回值

接口返回执行减1操作后的值(number类型)

注意

操作的值并非整数形式(例:字符串"1"是整数形式,字符串"a"非整数形式)时会直接抛出错误

示例

await redis.set('string-key', '1')
await redis.decr('string-key') // 0

# decrby

在指定的键上减一个整数

接口形式

await redis.decrby(key: string, decrement: number)

入参说明

参数 说明 必填 说明
key
decrement 减少的值

返回值

接口返回执行加一操作后的值(number类型)

注意

操作的值并非整数形式(例:字符串"1"是整数形式,字符串"a"非整数形式)时会直接抛出错误

示例

await redis.set('string-key', '1')
await redis.decrby('string-key', 2) // -1

# rpush

在List类型数据结尾追加数据

接口形式

await redis.rpush(key: string, value: string)

入参说明

参数 说明 必填 说明
key
value 追加的值

返回值

接口返回执行追加操作后List的长度

注意

  • 如果操作的数据类型不为List,则会抛出错误
  • 如果指定的key不存在,则创建一个新的List并将value追加进去

# rpushx

用法同rpush,仅在list存在时才在List结尾追加数据

# rpop

从List类型数据结尾删除一条数据,并返回删除的值

注意:redis内List长度为0时会被自动删除

接口形式

await redis.rpop(key: string)

入参说明

参数 说明 必填 说明
key

返回值

接口返回此次操作删除的值,如果key不存在则返回null

注意

  • 如果操作的数据类型不为List,则会抛出错误

# lpush

在List类型数据开头追加数据

接口形式

await redis.lpush(key: string, value: string)

入参说明

参数 说明 必填 说明
key
value 追加的值

返回值

接口返回执行追加操作后List的长度

注意

  • 如果操作的数据类型不为List,则会抛出错误
  • 如果指定的key不存在,则创建一个新的List并将value追加进去

# lpushx

用法同lpush,仅在list存在时才在List开头追加数据

# lpop

从List类型数据开头删除一条数据,并返回删除的值

注意:redis内List长度为0时会被自动删除

接口形式

await redis.rpop(key: string)

入参说明

参数 说明 必填 说明
key

返回值

接口返回此次操作删除的值,如果key不存在则返回null

注意

  • 如果操作的数据类型不为List,则会抛出错误

# linsert

在List内指定元素位置前或后插入元素,未匹配到指定元素时不插入

接口形式

await redis.linsert(key: string, dir: 'BEFORE' | 'AFTER', pivot: string, value: string)

入参说明

参数 说明 必填 说明
key
dir 指定在前还是后插入
pivot 指定要查找的元素
value 指定要插入的值

返回值

接口返回插入后的list长度,未匹配到要查找的值时返回-1,key不存在时此接口返回0

注意

  • 如果操作的数据类型不为List,则会抛出错误

# lindex

获取List内指定下标的元素

接口形式

await redis.lindex(key: string, index: number)

入参说明

参数 说明 必填 说明
key
index 指定下标

返回值

接口返回指定下标在list内对应的值,如果key不存在则返回null

注意

  • 如果操作的数据类型不为List,则会抛出错误

# llen

返回List的长度

接口形式

await redis.llen(key: string)

入参说明

参数 说明 必填 说明
key

返回值

接口返回list的长度,如果key不存在则返回0

注意

  • 如果操作的数据类型不为List,则会抛出错误

# exists

判断一个键是否存在

接口形式

await redis.exists(key: string)

入参说明

参数 说明 必填 说明
key

返回值

如果key存在返回数字1,如果key不存在返回数字0

示例

await redis.exists('string-key') // 0 | 1

# expire

为指定的key设置过期时间

接口形式

await redis.expire(key: string, seconds: number)

入参说明

参数 说明 必填 说明
key
seconds 过期时间 单位:秒

返回值

如果成功设置过期时间返回数字1,如果未成功存在返回数字0

示例

await redis.expire('key', 600) // 设置key为600秒后过期

# ttl

获取过期时间剩余多少秒

接口形式

await redis.ttl(key: string)

入参说明

参数 说明 必填 说明
key

返回值

如果没有设置过期时间(永久有效)返回数字-1,如果不存在或者已过期返回数字-2,否则返回剩余秒数

示例

await redis.ttl('key')

# multi

将多条指令作为一个原子执行。

示例

const multi = redis.multi()
multi.set('key1', 'value1')
multi.set('key2', 'value2')
multi.set('key3', 'value3')
multi.set('key4', 'value4')
const res = await multi.exec()

// 如果执行成功
res = ['OK','OK','OK','OK']

// 某个操作出现错误
res = ['OK','OK', error, 'OK'] // error为 Error对象的实例

# 执行lua脚本

接口形式

await redis.eval(String script, Number argsCount, String key1, String key2 , ... , String arg1, String arg2, ...)

参数说明

参数 类型 必填 说明
script String lua脚本内容
argsCount Number 参数个数,没有参数则传0
key1、key2... String 从eval的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)
arg1、agr2... Number 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)

某些情况下需要使用复杂的原子操作以避免高并发下数据修改混乱的问题,这种需求一般可通过执行lua脚本实现。如以下示例,判断redis中不存在key-test时,将其值设置为1;存在且小于10时进行加一操作;大于等于10时不进行操作直接返回。

{0, 1}是lua内的table类型,返回到云函数时会转为数组对应的值为[0, 1]

const [operationType, currentValue] = await redis.eval(`local val = redis.call('get','key-test')
    local valNum = tonumber(val)
    if (val == nil) then
        redis.call('set', 'key-test', 1)
        return {0, 1}
    end
    if (valNum < 10) then
        redis.call('incrby', 'key-test', 1)
        return {1, valNum + 1}
    else
        return {2, valNum}
    end
    `, 0)

# quit

断开redis连接,会等待redis请求执行完成后才断开连接

接口形式

await redis.quit()

入参说明

返回值

调用成功后返回OK字符串

注意

  • 断开连接后使用uniCloud.redis()返回的redis实例的连接将不再可用,再下次用到redis方法时需要重新调用uniCloud.redis()方法建立连接

# FAQ

  • 云函数与redis的连接

    和传统开发不同,云函数实例之间是不互通的,也就是说每个使用redis的函数实例都会和redis建立一个连接,在云函数实例复用时此连接也会复用。

# 最佳实践

# 高并发下抢购/秒杀/防超卖示例

可以利用redis的原子操作保证在高并发下不会超卖,以下为一个简单示例

在抢购活动开始前可以将商品库存同步到redis内,实际业务中可以通过提前访问一次抢购页面加载所有商品来实现。下面通过一个简单的演示代码来实现

const redis = uniCloud.redis()

const goodsList = [{ // 商品库存信息
  id: 'g1',
  stock: 100
}, {
  id: 'g2',
  stock: 200
}, {
  id: 'g3',
  stock: 400
}]

const stockKeyPrefix = 'stock_'

async function init() { // 抢购活动开始前在redis初始化库存
  for (let i = 0; i < goodsList.length; i++) {
    const {
      id,
      stock
    } = goodsList[i];
    await redis.set(`${stockKeyPrefix}${id}`, stock)
  }
}

init()

抢购的逻辑见以下代码

exports.main = async function (event, context) {
	// 一些判断抢购活动是否开始/结束的逻辑,未开始直接返回
	const cart = [{ // 购物车信息,此处演示购物车抢购的例子,如果抢购每次下单只能购买一件商品,可以适当调整代码
		id: 'g1', // 商品id
		amount: parseInt(Math.random() * 5 + 5) // 购买数量
	}, {
		id: 'g2',
		amount: parseInt(Math.random() * 5 + 5)
	}]

	/**
	* 使用redis的eval方法执行lua脚本判断各个商品库存能否满足购物车购买的数量
	* 如果不满足,返回库存不足的商品id
	* 如果满足,返回一个空数组
	* 
	* eval为原子操作可以在高并发下保证不出错
	**/ 
	let checkAndSetStock = `
	local cart = {${cart.map(item => `{id='${item.id}',amount=${item.amount}}`).join(',')}}
	local amountList = redis.call('mget',${cart.map(item => `'${stockKeyPrefix}${item.id}'`).join(',')})
	local invalidGoods = {}
	for i,cartItem in ipairs(cart) do
		if (cartItem['amount'] > tonumber(amountList[i])) then
		  table.insert(invalidGoods, cartItem['id'])
		  break
		end
	end
	if(#invalidGoods > 0) then
		return invalidGoods
	end
	for i,cartItem in ipairs(cart) do
		redis.call('decrby', '${stockKeyPrefix}'..cartItem['id'], cartItem['amount'])
	end
	return invalidGoods
	`
	const invalidGoods = await redis.eval(checkAndSetStock, 0)
	if (invalidGoods.length > 0) {
		return {
		  code: -1,
		  message: `以下商品库存不足:${invalidGoods.join(',')}`, // 此处为简化演示代码直接返回商品id给客户端
		  invalidGoods // 返回库存不足的商品列表
		}
	}
	// redis库存已扣除,将库存同步到数据库。为用户创建订单
	// 此处代码省略...
	return {
		code: 0,
		message: '下单成功,跳转订单页面'
	}
}