Redis介绍

        Redis 是Remote Dictionary Service 的简称;也是远程字典服务; Redis 是内存数据库,KV数据库,数据结构数据库; Redis 应用非常广泛,如Twitter、暴雪娱乐、Github、Stack Overflow、腾讯、阿里巴巴、京 东、华为、新浪微博等,很多中小型公司也在使用。Redis命令请查看:Redis命令中心(Redis commands) -- Redis中国用户组(CRUG)

        Redis的在生活中常见的应用有:

  • 记录朋友圈点赞数、评论数和点击数(hash)
  • 记录朋友圈说说列表(排序),便于快速显示朋友圈(zset)
  • 记录文章的标题、摘要、作者和封面,用于列表页展示(hash)
  • 记录朋友圈的点赞用户ID列表,评论ID列表,用于显示和去重计数(zset)
  • 缓存热点数据,减少数据库压力(hash)
  • 如果朋友圈说说ID是整数id,可使用redis来分配朋友圈说说id(计数器)(string)
  • 通过集合(set)的交并差集运算来实现记录好友关系(set)
  • 游戏业务中,每局战绩存储(list)

Redis安装编译

git clone https://gitee.com/mirrors/redis.git -b 6.2
cd redis
make
make test
make install
# 默认安装在 /usr/local/bin
# redis-server 是服务端程序
# redis-cli 是客户端程序

Redis启动

mkdir redis-data
# 把redis文件夹下 redis.conf 拷贝到 redis-data
# 修改 redis.conf
# requirepass 修改密码 123456
# daemonize yes
cd redis-data
redis-server redis.conf
# 通过 redis-cli 访问 redis-server
redis-cli -h 127.0.0.1 -a 123456

Redis数据库

        通过Redis客户端连接工具可以看到,Redis默认有16个数据库。由于Redis不支持自定义数据库的名字,所以每个数据库都以编号命名。

如何用redis存储用户登录信息 redis登陆命令指定用户_数据库

其实通过redis配置文件 redis.conf 也可以看出默认是16个数据库:

[root@usr]# cat redis.conf | grep 16
# bind 192.168.1.100 10.0.0.1
databases 16
# -3: max size: 16 Kb  <-- probably not recommended
# 16 bytes header. When an HyperLogLog using the sparse representation crosses
# A value greater than 16000 is totally useless, since at that point the
# 16 megabytes / 10 seconds, the client will get disconnected immediately
# disconnected if the client reaches 16 megabytes and continuously overcomes

        我们连接redis默认使用的是0号数据库,可以通过命令 select dbid 进行切换,编号是 0 至 databases-1:

如何用redis存储用户登录信息 redis登陆命令指定用户_数据库_02

        Redis不支持为每个数据库设置不同的访问密码,Redis只有一个密码,一个客户端要么可以访问全部数据库,要么全部数据库都没有权限。

        平常开发中,我们一般默认使用0号数据库,当然你也可以选择其他编号的数据库来进行数据存储。但是不同数据库的数据是不共享的,同一个数据库内的key不可以重复,但是不同数据库的键可以重复

        在关系型数据库(例如Mysql)中,我们一般用不同的数据库存储不同应用程序的数据,但是对于Redis的数据库,它更像是一种命名空间,不推荐用不同数据库来存储区分不同应用程序的数据。

        比如我们可以用0号数据库存储生产环境中的数据,使用1号数据库存储测试环境中的数据,但不适宜使用0号数据库存储应用A的数据,而使用1号数据库应用B的数据。我们更推荐的是不同的应用使用不同的Redis实例存储数据。因为Redis极其轻量级,一个空Redis实例占用的内存只有几M左右,所以不用担心多个Redis实例会额外占用很多内存。

        清空当前数据库所有数据命令为 FLUSHDB ,清空所有数据库的所有数据命令为 FLUSHALL。请注意以上所说的都是基于单机Redis的情况。在集群的情况下不支持使用select命令来切换db,因为Redis集群模式下只有一个db0。

常见的Redis数据结构

如何用redis存储用户登录信息 redis登陆命令指定用户_redis_03

Redis数据类型和编码方式

        redis无论什么数据类型,在数据库中都是以key-value形式保存,并且所有的key(键)都是字符串,所以讨论基础数据结构都是讨论的value值的数据类型。

数据类型

结构存储的值

结构的读写能力

String字符串

可以是字符串、整数浮点数

对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作;

List列表

一个链表,链表上的每个节点都包含一个字符串

对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或删除元素;

Set集合

字符串的无序集合

字符串的集合,包含基础的方法有看是否存在添加、获取、删除;还包含计算交集、并集、差集等

Hash散列表

键值对的无序散列表

包含方法有添加、获取、删除单个元素

Zset有序集合

和散列一样,用于存储键值对

字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定;包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素

Redis支持五种主要的数据类型:字符串(string)、列表(list)、集合(set)、有序集合(sorted set)和哈希(hash)。每种数据类型都有对应的编码方式。数据类型与编码方式总览如下:

数据类型

编码方式

字符串

int、embstr、raw

列表

ziplist、linkedlist、quicklist

哈希表

ziplist、hashtable

集合

intset、hashtable

有序集合

ziplist、skiplist

我们可以使用type key命令查看key对应的value的数据类型;使用object encoding key查看key对应value的编码类型。它们的区别如下:

  • type命令返回的是key的对外数剧结构类型,就是我们说的五种类型:string(字符串),hash(哈希),list(列表),set(集合),zset(有序集合)。
  • object encoding 命令返回的是redis存储value的内部编码。

如何用redis存储用户登录信息 redis登陆命令指定用户_字符串_04

如何用redis存储用户登录信息 redis登陆命令指定用户_字符串_05

如何用redis存储用户登录信息 redis登陆命令指定用户_如何用redis存储用户登录信息_06

string

         字符数组,该字符串是动态字符串,字符串长度小于1M时,加倍扩容;超过1M每次只多扩1M; 字符串最大长度为512M。

注意:redis字符串是二进制安全字符串;可以存储图片,二进制协议等二进制数据;'\0'在redis里并非是字符串结束的表计,字符串长度才是。

String是redis最基本的类型,一个key对应一个value。

  • String类型是二进制安全的,意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象。
  • String类型是redis最基本的数据类型,一个redis中字符串value最多可以是512M

基础命令

#列出所有的key
keys *
#列出匹配的key,如appxxx
keys app*
# 设置 key 的 value 值
SET key val
# 获取 key 的 value
GET key
# 执行原子加一的操作
INCR key
# 执行原子加一个整数的操作(value须是整数。INCRBY key 10)
INCRBY key increment
# 执行原子减一的操作(value须是整数)
DECR key
# 执行原子减一个整数的操作(value须是整数)
DECRBY key decrement
# 如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做
SETNX key value
# 删除 key val 键值对
DEL key
# 设置或者清空key的value(字符串)在offset处的bit值。
SETBIT key offset value
# 返回key对应的string在offset处的bit值
GETBIT key offset
# 统计字符串被设置为1的bit数.
BITCOUNT key
#获取[begin,end]下标范围内的值,如果是(0,1)就是获取所有值
getrange key begin end
#从begin下标开始设置xxx值,将原有的替换掉 
setrange key begin xxxx

#设置键过期时间
setex key seconds expire
#查看key剩余存活时间
ttl key
#同时设置或获取多个key-value
met key1 value1 key2 value2:用于同时设置一个或多个 key-value 对
mget key1 key2 :返回所有(一个或多个)给定 key 的值(如果某个key不存在,不存在的key返回null)
msetnx key1 value1 key2 value2:当所有 key 都成功设置,返回 1 。 如果有一个key设置失败,所有的key设置都会失败,返回 0 。原子操作

Value存储结构

  • 字符串长度<= 20 且能转成整数,则使用 int 存储;
  • 字符串长度<= 44,则使用 embstr 存储(嵌入式字符串);
  • 字符串长度>44,则使用 raw 存储;

redis会根据当前值的类型长度去判断选用那种内部编码实现。

为什么redis字符串存储小于等于44字节时,是 embstr 类型,而超过44是 raw 类型

redis中所有的value均用redisObject保存,其结构如下:

typedef struct redisObject {
	unsigned type : 4;
	unsigned encoding : 4;	 //type + encoding = 8bit, 1个字节
	unsigned lru : LRU_BITS; //24bit, 3个字节
	int refcount;			 //4字节
	void *ptr;				 //8字节,指向sdshdr8
} robj;
struct __attribute__((__packed__)) sdshdr8 {
	uint8_t len; //1字节
	uint8_t alloc; //1字节
	unsigned char flags; //1字节
	char buf[];			//字符数组要预留'\0', 至少1个字节
};

 答:redis 内存分配器认为大于 64个字节为大字符串;所以留给小字符串的大小为 64 - 16 - 3 - 1 = 44 ;

应用

对象存储

SET role:10001 '{["name"]:"mark",["sex"]:"male",["age"]:30}'
GET role:10001

如何用redis存储用户登录信息 redis登陆命令指定用户_字符串_07

 累加器

# 统计阅读数 累计加1(如果没有reads, 则创建read令其值为0并加1)
incr reads
# reads加100
incrby reads 100

分布式锁

# 加锁
setnx lock 1
# 释放锁
del lock

如何用redis存储用户登录信息 redis登陆命令指定用户_数据库_08

        每个线程在修改num时,先执行setnx lock 1命令,如果执行成功,说明此时没有线程占有num,可以修改num的值,执行完之后再执行del lock,删除lock;如果执行失败,说明此时num正被别的线程占有,因此不能修改num。这利用了setnx对已存在的key无法进行修改,只能修改未存在的key。

位运算

setbit的作用:对key上存储的字符串,设置或清除指定偏移量上的位(bit)。

语法:SETBIT key offset value

        key是要操作的对象的键。

        offset是操作对象上的偏移量,从左往右计数的,也就是从高位往低位。

        value,只能是0或1

SETBIT key offset value 的返回值是指定偏移位上原来的值(0或1)。如果没有设置key指定位置的值,初始值或默认值是0。

        在redis中,存储的字符串都是以二级制进行存在的。举个例子:我们设置(key, value)=(word, a),我们知道 'a' 的ASCII码是 97。转换为二进制是:01100001。offset的学名叫做“偏移” 。二进制中的每一位就是offset值啦,比如在这里 offset 0 等于 ‘0’ ,offset 1等于'1' ,offset2等于'1',offset 6 等于'1' ,没错,offset是从左往右计数的,也就是从高位往低位。为了将'a'变成'b',也就是将 01100001 变成 01100010 (b的ASCII码是98),这个很简单啦,也就是将'a'中的offset 6从0变成1,将offset 7 从1变成0 。

如何用redis存储用户登录信息 redis登陆命令指定用户_缓存_09

BITCOUNT 就是统计字符串的二级制码中,有多少个'1'。

GETBIT key offset value:获取key上存储值的第offset个bit位上的值

# 月签到功能 10001 用户id 202106 2021年6月份的签到 6月份的第1天
setbit sign:10001:202106 1 1
# 计算 2021年6月份 的签到情况
bitcount sign:10001:202106
# 获取 2021年6月份 第二天的签到情况 1 已签到 0 没有签到
getbit sign:10001:202106 2

redis的setbit命令,能应用在布隆过滤器中。

list

基础命令

Redis列表是简单字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头或则尾
它的底层是一个双向链表,对两端操作的性能很高,通过索引下标操作中间节点的性能很差。

如何用redis存储用户登录信息 redis登陆命令指定用户_字符串_10

# 从队列的左侧入队一个或多个元素
LPUSH key value [value ...]
# 从队列的左侧弹出一个元素。当列表 key 不存在时,返回 nil 。
LPOP key
# 从队列的右侧入队一个或多个元素
RPUSH key value [value ...]
# 从队列的右侧弹出一个元素,返回值为移除的元素
RPOP key
#按照索引下标获得元素(-1代表最后一个,0代表是第一个)
LINDEX key index
#返回列表的长度
LLEN key
# 返回从队列的 start 和 end 之间的元素,其中 0 表示第一个元素,-1表示最后一个元素
LRANGE key start end
# 从存于 key 的列表里移除前 count 次出现的值为 value 的元素
LREM key count value
#让列表只保留指定区间内的元素,其余元素全被删除。下标0表示第一个元素
LTRIM key start end
#移除列表key1的最后一个元素,并将该元素添加到key2列表并返回
rpoplpush key1 key2
#判断列表是否存在:存在返回1,不存在返回0
exists key
#将列表 key 下标为 index 的元素的值设置为 value
lset key index value
#将值 value 插入到列表 key 当中,位于值 value0 之前或之后。
linsert key before/after value0 value
# 它是 RPOP 的阻塞版本,因为这个命令会在给定list无法弹出任何元素的时候阻塞连接
BRPOP key timeout

如何用redis存储用户登录信息 redis登陆命令指定用户_缓存_11

Value存储结构

  • ziplist(压缩列表):当列表元素个数小于list-max-ziplist-entries配置(默认512个),同时所有值都小于list-max-ziplist-value配置(默认64字节),redis会使用ziplist作为列表的内部实现来减少内存的使用。(它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当元素长度小于48字节或者元素压缩前后长度差不超过8,不压缩)
  • quicklist(双向链表):当数据量多的时候会用quicklist。Redis将链表和ziplist结合起来组成了quicklist。(也就是将多个ziplist使用双向指针连接起来,这样即满足了快速插入删除的功能,又不会出现太大的空间冗余)

如何用redis存储用户登录信息 redis登陆命令指定用户_字符串_12

 

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz; /* ziplist size in bytes */
    unsigned int count : 16; /* count of items in ziplist */
    unsigned int encoding : 2; /* RAW==1 or LZF==2 */
    unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small*/
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count; /* total count of all entries in all ziplists */
    unsigned long len; /* number of quicklistNodes */
    int fill : QL_FILL_BITS; /* fill factor for individual nodes */
    unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

应用

栈(先进后出 FILO)

LPUSH + LPOP
# 或者
RPUSH + RPOP

队列(先进先出 FIFO) 

LPUSH + RPOP
# 或者
RPUSH + LPOP

阻塞队列(blocking queue)

LPUSH + BRPOP
# 或者
RPUSH + BLPOP

异步消息队列

操作与队列一样,但是在不同系统间;

如何用redis存储用户登录信息 redis登陆命令指定用户_缓存_13

 获取固定窗口记录(战绩)

比如:利用ltrim裁剪获取最近5条记录
ltrim games 0 4
lrange games 0 -1


实际项目中需要保证命令的原子性,所以一般用 lua 脚本 或者使用 pipeline 命令;
-- redis lua脚本
local record = KEYS[1]
redis.call("LPUSH", "games", record)
redis.call("LTRIM", "games", 0, 4)

hash

        Redis hash 是一个 string 类型的 field(字段) 和 value(属性) 的映射表,hash 特别适合用于存储对象。一个hash可以存多个key-value,类似一个对象的多个字段和属性。

基础命令

# 获取 key 对应 hash 中的 field 对应的值
HGET key field
# 设置 key 对应 hash 中的 field 对应的值
HSET key field value
# 设置多个hash键值对
HMSET key field1 value1 field2 value2 ... fieldn valuen
# 获取多个field的值
HMGET key field1 field2 ... fieldn
# 给 key 对应 hash 中的 field 对应的值加一个整数值
HINCRBY key field increment
#返回哈希表key中,所有的字段和值
hgetall key
# 获取 key 对应的 hash 有多少个键值对
HLEN key
# 删除 key 对应的 hash 的键值对,该键为field
HDEL key field

#查看哈希表myhash的指定字段field1是否存在:存在返回1,不存在返回0
hexists myhash field1

#获取哈希表myhash中的所有field
hkeys myhash

#返回哈希表myhash所有field的值
hvals myhash

#为哈希表中不存在的的字段赋值,如果字段存在,则设置失败
hsetnx myhash field value

如何用redis存储用户登录信息 redis登陆命令指定用户_缓存_14

Value存储结构

        哈希对象的键是一个字符串类型,值是一个键值对集合。哈希对象的编码可以是 ziplist 或者 hashtable

  • hashtable:节点数量大于 512(hash-max-ziplist-entries) 或所有字符串长度大于 64(hash-max-ziplist-value),则使用 hashtable实现;
  • ziplist:节点数量小于等于 512 且有一个字符串长度小于 64,则使用 ziplist 实现;

应用

存储对象

        例如存储一个人的信息:

set hash:10001 '{["name"]:"liming",["sex"]:"male",["age"]:18}'

购物车

# 将用户id作为 key
# 商品id作为 field
# 商品数量作为 value
# 注意:这些物品是按照我们添加顺序来显示的;

# 添加商品:
hset MyCar:10001 40001 1

# 增加数量:
hincrby MyCar:10001 40001 1

# 显示所有物品数量:
hlen MyCar:10001

# 删除商品:
hdel MyCar:10001 40001

# 获取所有物品:
hget MyCar:10001 40001
hget MyCar:10001 40002
hget MyCar:10001 40003

set

Redis 的 Set 是 String 类型的无序集合,其特点如下:

  • 集合成员是唯一的
  • Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)

基础命令

# 添加一个或多个指定的member元素到集合的 key中
SADD key member [member ...]
# 计算集合元素个数
SCARD key
# 获取集合key中的所有元素值
SMEMBERS key
# 返回成员 member 是否是存储的集合 key的成员
SISMEMBER key member
# 随机返回key集合中的一个或者多个元素,不删除这些元素
SRANDMEMBER key [count]
# 从存储在key的集合中移除并返回一个或多个随机元素
SPOP key [count]
# 返回一个集合与给定集合的差集的元素
SDIFF key [key ...]
# 返回指定所有的集合的成员的交集
SINTER key [key ...]
# 返回给定的多个集合的并集中的所有成员
SUNION key [key ...]

Value存储结构

  • intset:当集合中的元素都为整数且节点数量小于等于 512(set-max-intset-entries),则使用整数数组存储;
  • hashtable:当集合中的元素有一个不是整数或者节点数量大于 512,则使用hashtable(也叫字典)存储;

应用

抽奖

# 添加抽奖用户
sadd Award:1 10001 10002 10003 10004 10005 10006
sadd Award:1 10009
# 查看所有抽奖用户
smembers Award:1
# 抽取多名幸运用户
srandmember Award:1 10

共同关注

sadd follow:A mark king darren mole vico
sadd follow:C mark king darren
#返回集合follow:A和集合follow:C的交集
sinter follow:A follow:C

推荐好友

sadd follow:A mark king darren mole vico
sadd follow:C mark king darren
# C可能认识的人:
sdiff follow:A follow:C

zset

        Redis 的 zset 是 String 类型的有序集合,每个元素都会关联一个 double 类型的权重参数(score)集合中的元素可以按score进行有序排列。其特点如下:

  • 集合的成员是唯一的,但分数(score)却可以重复
  • 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)

基础命令

# 将一个或多个成员元素及其分数值加入到有序集当中
ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
# 从键为key有序集合中删除 member 的键值对
ZREM key member [member ...]
# 返回有序集key中,成员member的score值
ZSCORE key member
# 为有序集key的成员member的score值加上增量increment
ZINCRBY key increment member
# 返回key的有序集元素个数
ZCARD key
# 计算有序集合中指定分数区间的成员数量。
zcount key score1 score2
# 返回有序集key中成员member按分数值递增的排名
ZRANK key member
返回有序集中成员member按分数值递减(从大到小)排名
zrevrank key member

# 返回存储在有序集合key中的指定范围的元素
ZRANGE key start stop [WITHSCORES]
# 返回有序集key中,指定区间内的成员(逆序)
ZREVRANGE key start stop [WITHSCORES]

#返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大)次序排列。
# Inf无穷大量+∞,同样地,-∞可以表示为-Inf。
ZRANGEBYSCORE key -inf +inf    # 显示整个有序集
ZRANGEBYSCORE key -inf +inf withscores # 递增排列
ZRANGEBYSCORE key -inf 2500 WITHSCORES #显示score属于(-∞, 2500]的成员

如何用redis存储用户登录信息 redis登陆命令指定用户_redis_15

Value存储结构

  • ziplist(压缩列表):当列表元素个数小于等于zset-max-ziplist-entries配置(默认128字节),同时所有字符串长度都小于等于zset-max-ziplist-value配置(默认64字节),则使用ziplist存储;
  • skiplist(跳跃表): 当ziplist条件不满足时,则使用skiplist存储,因为此时ziplist的读写效率会下降

应用

# 点击新闻:
zincrby hot:20210203 1 10001
zincrby hot:20210203 1 10002
zincrby hot:20210203 1 10003
zincrby hot:20210203 1 10004
zincrby hot:20210203 1 10005
zincrby hot:20210203 1 10006
zincrby hot:20210203 1 10007
zincrby hot:20210203 1 10008
zincrby hot:20210203 1 10009
zincrby hot:20210203 1 10010
# 获取排行榜:
zrevrange hot:20210203 0 9 withscores

延时队列

        将消息序列化成一个字符串作为 zset 的member;这个消息的到期处理时间作为score,然后用多 个线程轮询zset获取到期的任务进行处理。

def delay(msg):
    msg.id = str(uuid.uuid4()) #保证 member 唯一
    value = json.dumps(msg)
    retry_ts = time.time() + 5 # 5s后重试
    redis.zadd("delay-queue", retry_ts, value)
# 使用连接池
def loop():
    while True:
        values = redis.zrangebyscore("delay-queue", 0, time.time(), start=0,num=1)
        if not values:
            time.sleep(1)
            continue
        value = values[0]
        success = redis.zrem("delay-queue", value)
        if success:
            msg = json.loads(value)
            handle_msg(msg)
# 缺点:loop 是多线程竞争,两个线程都从zrangebyscore获取到数据,但是zrem一个成功一个失
败,
# 优化:为了避免多余的操作,可以使用lua脚本原子执行这两个命令
# 解决:漏斗限流

分布式定时器

如何用redis存储用户登录信息 redis登陆命令指定用户_如何用redis存储用户登录信息_16

        生产者将定时任务 hash 到不同的 redis 实体中,为每一个redis实体分配一个dispatcher进程,用 来定时获取redis中超时事件并发布到不同的消费者中;

时间窗口限流

        系统限定用户的某个行为在指定的时间里只能发生N次:

# 指定用户 user_id 的某个行为 action 在特定时间内 period 只允许发生做多的次数max_count
def is_action_allowed(userid, action, period, max_count):
    key = 'hist:%s:%s' % (userid, action)
    now_ts = int(time.time()*1000) # 毫秒时间戳
    with client.pipeline() as pipe:
        # 记录行为
        pipe.zadd(key, now_ts, now_ts)
        # 移除时间窗口之前的行为记录,剩下的都是时间窗口内的
        pipe.zremrangebyscore(key, 0, now_ts - period * 1000)
        # 获取时间窗口内的行为数量
        pipe.zcard(key)
        # 设置过期时间,避免冷用户持续占用内存 时间窗口的长度+1秒
        pipe.expire(key, period + 1)
        _,_,current_count,_ = pipe.execute()
    return current_count <= max_count

can_reply = is_action_allowed(10001, "replay", 60, 5)
if can_reply:
    do_reply()
else:
    raise ActionThresholdOverflow()
# 维护一次时间窗口,将窗口外的记录全部清理掉,只保留窗口内的记录;
# 缺点:记录了所有时间窗口内的数据,如果这个量很大,不适合做这样的限流;

参考文献:Redis五种基本数据类型