Day15 Redis基础

2022/4/23 19:12:54

本文主要是介绍Day15 Redis基础,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

01 关于数据库的客户端介绍

关系型数据库

特点:

1 全部使用SQL(结构化查询语句)进行数据操作

2 都存在主外键关系,表 等等关系特征

3 大部分都支持各种关系型数据库的特性:事务、存储过程、触发器、视图、临时表、模式、函数

非关系型数据库NoSQL

典型:Redis、MongoDB、hbase、 Hadoop、elasticsearch、图数据库(Neo4j、GraphDB、SequoiaDB)

 

特点:

每一款都不一样。用途不一致,功能不一致,各有各的操作方式。

基本不支持主外键关系,也没有事务的概念。(MongoDB号称最接近关系型数据库的,所以MongoDB有这些)

 

Redis (Remote Dictionary Server 远程字典服务)

memcache 缺点:值为字符串 不能持久化

 

 

从数据库拿数据,涉及IO操作。影响性能。

机械硬盘和内存性能差100倍

内容网站,

不需要读写数据库,放入内存中。

数据库更多的起到备份的功能。

还需要涉及如何同步问题。

Redis特性:

速度快

持久化

多种数据结构

支持多种编程语言:CS架构

功能丰富

简单: 代码短小精悍

主从赋值

高可用、分布式

 

 

本质如何访问mysqld的?

 

Mysql提供对外的接口,本质上基于socket的基础上实现。

先接受参数,验证通过了,才执行指令

 

 

 

存储数据结构不同

表存储

k:value存储

队列存储

 

Python作为生产者 生产数据, go作为消费者 消费数据

02 Redis安装

 

 

 

 

 

 

 

 

C:\Users\zhu>redis-cli

127.0.0.1:6379> set name user1

OK

127.0.0.1:6379> get name

"user1"

127.0.0.1:6379>

 

03 数据类型 5种

value类型 key:value

String //字符串

hash类型 {“”:””,} //类似字典

list类型  [] //列表

set类型{1,2,3} //集合  无序,去重

zset {1,2,3} //有序集合

 

d={“k1”:{}}

Redis大的字段

Redis= {

“”:””,

“”:[],

“”:{},

“”:{}

}

 

Python 值可以任意类型,键 可hash

Redis 键是字符串,根据值不同,相关方法

 

概述

 

redis就是一个全局的大字典,key就是数据的唯一标识符。根据key对应的值不同,可以划分成5个基本数据类型。

1. string类型:

字符串类型,是 Redis 中最为基础的数据存储类型,它在 Redis 中是二进制安全的,也就是byte类型。

单个数据的最大容量是512M。

key: 值

2. hash类型:

哈希类型,用于存储对象/字典,对象/字典的结构为键值对。key、域、值的类型都为string。

key:{

            域(属性): 值,

            域:值,            

            域:值,

            域:值,

            ...

}

3. list类型:存值,有顺序)

列表类型,它的子成员类型为string。

key: [值1,值2, 值3.....]

4. set类型:(存值,无序,不重复)

无序集合,它的子成员类型为string类型,元素唯一不重复,没有修改操作。

key: {值1, 值4, 值3, ...., 值5}

5. zset类型(sortedSet): (存值 有权重 值有顺序)

有序集合,它的子成员值的类型为string类型,元素唯一不重复,没有修改操作。权重值(score,分数)从小到大排列。

key: {

值1 权重值1(数字);

值2 权重值2;

值3 权重值3;

值4 权重值4;

}

 

1 redis值为string(1)的操作方法

设置键值set key value

127.0.0.1:6379> set name user1

获取键值get key

127.0.0.1:6379> get name

 

127.0.0.1:6379> keys *

127.0.0.1:6379> scan 0

setnx 键不存在,才设置成功

set 默认覆盖的效果

127.0.0.1:6379> del name1

127.0.0.1:6379> setnx name1 user1

127.0.0.1:6379> get name1

 

 

键不存在,执行成功,返回integer 1

127.0.0.1:6379> setnx age 22

127.0.0.1:6379> get age

 

 

setex 设置键值的过期时间

127.0.0.1:6379> setex gender 10 male

127.0.0.1:6379> get gender

过了10秒,再查看已过期

 

总结

# 1 redis值为string的操作方法

set key value 设置键值

get key 获取某键的值

setnx key value # 当键存在则无效

setex key seconds value # 设置有效时间的键值对

 

 

设置多个键值

127.0.0.1:6379> mset height 170 weight 60kg

127.0.0.1:6379> get height

127.0.0.1:6379> mget height weight

 

127.0.0.1:6379> mset a1 golang a2 java a3 c

 

字符串拼接 append

返回总长度

127.0.0.1:6379> set article my

OK

127.0.0.1:6379> get article

my

127.0.0.1:6379> append article redis

7

127.0.0.1:6379> get article

myredis

127.0.0.1:6379> append article start

12

127.0.0.1:6379> get article

myredisstart

一次获取多个值

127.0.0.1:6379> mget name age

 

总结

# 1 redis值为string的操作方法

set key value 设置键值

get key 获取某键的值

setnx key value # 当键存在则无效

setex key seconds value # 设置有效时间的键值对

 

mset height 70 weight 60kg # 设置多个键值

mget name age # 获取多个键的值

 

 

 

自增自减 incr decr

应用场景:电商抢购,秒杀,投票,攻击计数,在线人数等

 

存储的字符串? 如何自增

127.0.0.1:6379> set goods_count 100

127.0.0.1:6379> get goods_count

 

 

存储必须为数值类型的字符串

127.0.0.1:6379> incr goods_count

127.0.0.1:6379> incrby goods_count 5

127.0.0.1:6379> decr goods_count

127.0.0.1:6379> decrby goods_count 5

 

获取字符串的长度strlen

127.0.0.1:6379> get name

user1

127.0.0.1:6379> strlen name

 

 

比特流

var x int8 1B字节=8比特bit

注意:一般说的1kb是1024字节

 

1个in8: 1个字节, 存2的8次方 256个值

1个中文:3个字节

 

 

SETBIT     # 设置一个bit数据的值

GETBIT     # 获取一个bit数据的值

BITCOUNT   # 统计字符串被设置为1的bit数.

BITPOS     # 返回字符串里面第一个被设置为1或者0的bit位。

 

 

127.0.0.1:6379> setbit mykey 7 1

127.0.0.1:6379> getbit mykey 7

127.0.0.1:6379> getbit mykey 6

 

 

应用案例

考勤 1出勤 0 未出勤

127.0.0.1:6379> bitcount mykey

第一次执行返回0,再次执行返回1

127.0.0.1:6379> setbit mykey 7 1

 

 

 

 

BITPOS

第一次设置为1或者0的比特位

127.0.0.1:6379> bitpos mykey 1

 

 

统计每个人的信息

不放到最外层,放到hash里边

redis = {

   "user_1":"",

   "":[],

   "login":{

    "user_1":""

   },

   "":""

}

 

练习

自增自减练习

127.0.0.1:6379> set goods_count 100

127.0.0.1:6379> get goods_count

127.0.0.1:6379> incr goods_count

127.0.0.1:6379> decr goods_count

04 key操作 与键的值的数据无关

查找键

127.0.0.1:6379> keys *

127.0.0.1:6379> keys a*

127.0.0.1:6379> keys *a

exists指令

判断某个key,存在返回1 不存在返回0

127.0.0.1:6379> exists name

1

127.0.0.1:6379> exists names

0

 

type key 查看键的值的数据类型

127.0.0.1:6379> type name

string

del key1 key2 删除某个或某些键值对

127.0.0.1:6379> del weight height

expire key 10 设置过期时间

# 给已经存在的某个键设置一个过期时间

127.0.0.1:6379> expire key seconds

127.0.0.1:6379> get name

127.0.0.1:6379> expire name 10

127.0.0.1:6379> get name

 

 

ttl查看某个key 剩余的有效期

不存在的键,用ttl也会返回-2

127.0.0.1:6379> ttl name

不支持通配

以下命令无效

127.0.0.1:6379> expire a* 3

0

 

flushall 清空redis的所有key

127.0.0.1:6379> flushall

 

 

rename 重命名

127.0.0.1:6379> set name user1

OK

127.0.0.1:6379> rename name u1

OK

127.0.0.1:6379> get name

 

127.0.0.1:6379> get u1

user1

 

总结

# 2 key操作:与键的值得数据内容无关

# 获取redis下所有的键

key *

# 判断某个key是否存在,存在返回1 不存在返回0

exists name

# 获取键的值得数据类型

type key

# 删除某个或某些键值对

del key1 key2

# 给已经存在的魔鬼键设置一个过期时间

expire key seconds

# 某个key剩余的有效期

ttl key

 

flushall # 清空redis的所有的key

rename oldkey newkey # 重命名

切换数据库

select

127.0.0.1:6379> select 1

 

 

 

下午05 redis值为hash(2)的操作方法

 

 

 

键key:{

    域field: 值value,

    域field: 值value,

    域field: 值value,

}

hset

hset key field value

127.0.0.1:6379> hset info name user1

 

 

 

 

hget

127.0.0.1:6379> hget info name

127.0.0.1:6379> hset info age 22

127.0.0.1:6379> hget info

ERR wrong number of arguments for 'hget' command

hgetall

127.0.0.1:6379> hgetall info

name

user1

age

22

 

redis = {

  "info":{

"name":"user1",

"age":"22"

}

}

 

hmset 老版本

127.0.0.1:6379> hmset info2 name user age 24

127.0.0.1:6379> hgetall info2

 

 

 

新版本 hset 可批量创建

127.0.0.1:6379> hset info3 name user3 age 22

2

127.0.0.1:6379> hgetall info3

name

user3

age

22

 

 

hkeys

127.0.0.1:6379> hkeys info3

name

age

hvals

127.0.0.1:6379> hvals info3

user3

22

 

 

 

用:下划线 都行

127.0.0.1:6379> hset user:1 name zhansan

1

127.0.0.1:6379> hgetall user:1

name

zhansan

 

hdel 删除键对应得域值对

127.0.0.1:6379> hgetall info3

name

user3

age

22

127.0.0.1:6379> hdel info3 age

1

127.0.0.1:6379> hgetall info3

name

user3

 

hexists 判断域值是否存在

Exists

存在返回1 不存在返回0

127.0.0.1:6379> exists info3

1

127.0.0.1:6379> hexists info3

ERR wrong number of arguments for 'hexists' command

 

127.0.0.1:6379> hexists info3 name

1

hincrby 自增自减

127.0.0.1:6379> hgetall info2

name

user

age

24

127.0.0.1:6379> hincrby info2 age 2

127.0.0.1:6379> hincrby info2 age -2

 

 

练习

127.0.0.1:6379> hset info3 age 18

1

127.0.0.1:6379> get info3

WRONGTYPE Operation against a key holding the wrong kind of value

 

127.0.0.1:6379> hget info3 age

18

127.0.0.1:6379> hgetall info3

name

user3

age

18

下午06 list(3)

lpush key value1 value2

rpush

127.0.0.1:6379> lpush names1 zhangsan lisi wangwu

3

127.0.0.1:6379> rpush names2 zhangsan lisi wangwu

3

lrange 支持正负索引

127.0.0.1:6379> lrange names1 0 -1

127.0.0.1:6379> lrange names2 0 -1

示例

127.0.0.1:6379> flushall

127.0.0.1:6379> rpush brother liu guan zhang

3

127.0.0.1:6379> lrange brother 0 -1

liu

guan

zhang

linsert key after|before 标杆值 插入值

向指定位置插入指定值

127.0.0.1:6379> linsert brother after guan lvbu

4

127.0.0.1:6379> lrange brother 0 -1

liu

guan

lvbu

zhang

lset 根据索引设置值

127.0.0.1:6379> lset brother 2  machao

OK

127.0.0.1:6379> lrange brother 0 -1

liu

guan

machao

zhang

 

 

lrem删除

删除指定成员

lrem key count value

 

# 注意:

# count表示删除的数量,value表示要删除的成员。该命令默认表示将列表从左侧前count个value的元素移除

# count==0,表示删除列表所有值为value的成员

# count >0,表示删除列表左侧开始的前count个value成员

# count <0,表示删除列表右侧开始的前count个value成员

 

值小于0 从右往左删

127.0.0.1:6379> lrange brother 0 -1

liu

guan

machao

zhang

user1

guanyu

user1

guanyu

user1

guanyu

127.0.0.1:6379> lrem brother -2 user1

2

127.0.0.1:6379> lrange brother 0 -1

liu

guan

machao

zhang

user1

guanyu

guanyu

guanyu

 

 

127.0.0.1:6379> del brother

1

127.0.0.1:6379> rpush brother A B A C A

5

127.0.0.1:6379> lrem brother 0 A

3

127.0.0.1:6379> lrange brother 0 -1

B

C

删右侧两个A

127.0.0.1:6379> del brother

1

127.0.0.1:6379> rpush brother A B A C A

5

127.0.0.1:6379> lrem brother -2 A

2

127.0.0.1:6379> lrange brother 0 -1

A

B

C

删左侧两个A

127.0.0.1:6379> del brother

1

127.0.0.1:6379> rpush brother A B A C A

5

127.0.0.1:6379> lrem brother 2 A

2

127.0.0.1:6379> lrange brother 0 -1

B

C

A

 

lindex 通过索引查找list中的元素

127.0.0.1:6379> del brother

1

127.0.0.1:6379> rpush brother liu guan zhang liu guan zhang

6

127.0.0.1:6379> lrange brother 0 -1

liu

guan

zhang

liu

guan

zhang

127.0.0.1:6379> lindex brother 3

liu

127.0.0.1:6379> lindex brother 2

zhang

 

 

 redis = {

   "names":["","",""]

 }

 

lpush key value1 value2 ...

rpush key value1 value2 ...

lrange key 0 -1 # 支持正负索引

 

# 向指定位置插入某个值

linsert key after|before 标杆值 插入值

 

# 根据索引设置值

lset brother 2 machao

 

# 根据内容删除list中元素

lrem brother -2 guanyu

# 通过索引查找list中元素

lindex brother 3

# 通过索引范围查找多个元素值

lrange key start stop

 

lpop 移除并获取列表第一个成员

移除并获取 获取:做返回值

 

remove一般按值删

pop一般按索引删

 

127.0.0.1:6379> lpop brother

 

rpop 移除并获取列表最后一个成员

127.0.0.1:6379> rpop brother

llen 获取列表的长度

llen key

回顾

rpush

lpop

配套使用

 

lrange 查

linx

lrem 删

lset 改

 

hget

hgetall

127.0.0.1:6379> hset info1 name user1

(integer) 1

127.0.0.1:6379> hset info1 age 18

(integer) 1

127.0.0.1:6379> hget info1 name

"user1"

127.0.0.1:6379> hget info1 age

"18"

127.0.0.1:6379> hgetall info1

1) "name"

2) "user1"

3) "age"

4) "18"

hset

127.0.0.1:6379> hmset info1 name:2 user2 age:2 18

OK

127.0.0.1:6379> hmset info1 name:3 user3 age:3 15

OK

127.0.0.1:6379> hset info1 name:4 user4 age:4 25

(integer) 2

 

hkeys

127.0.0.1:6379> hkeys info1

1) "name"

2) "age"

3) "name:2"

4) "age:2"

5) "name:3"

6) "age:3"

7) "name:4"

8) "age:4"

hvals

127.0.0.1:6379> hvals info1

1) "user1"

2) "18"

3) "user2"

4) "18"

5) "user3"

6) "15"

7) "user4"

8) "25"

下午07 set(4)无序 去重

sadd key val1 val2 构建一个set值

127.0.0.1:6379> sadd snames liubei guanyu zhangfei

 

 

smembers key 查询集合的所有值

127.0.0.1:6379> smembers snames

 

scard key 获取集合长度

127.0.0.1:6379> scard snames

 

 

 

spop key 随机删除集合键下的某个元素

127.0.0.1:6379> spop snames

 

 

srem key val1 按照值删除元素

 

127.0.0.1:6379> sadd snames liubei guanyu zhangfei

(integer) 3

127.0.0.1:6379> scard snames

(integer) 3

127.0.0.1:6379> srem snames guanyu

(integer) 1

127.0.0.1:6379> smembers snames

1) "liubei"

2) "zhangfei"

C:\Users\zhu>redis-cli --raw

127.0.0.1:6379> sadd snames liubei guanyu zhangfei

1

127.0.0.1:6379> scard snames

3

127.0.0.1:6379> srem snames guanyu

1

127.0.0.1:6379> smembers snames

liubei

zhangfei

交叉并集合

 

 

应用场景:推荐 协同过滤,基于用户,基于物品

sinter  key1 key2 key3 ....    # 交集、比较多个集合中共同存在的成员

sdiff   key1 key2 key3 ....    # 差集、比较多个集合中不同的成员

sunion  key1 key2 key3 ....    # 并集、合并所有集合的成员,并去重

 

 

del user:1 user:2 user:3 user:4

sadd user:1 1 2 3 4     # user:1 = {1,2,3,4}

sadd user:2 1 3 4 5     # user:2 = {1,3,4,5}

sadd user:3 1 3 5 6     # user:3 = {1,3,5,6}

sadd user:4 2 3 4       # user:4 = {2,3,4}

交集 sinter

# 交集

127.0.0.1:6379> sinter user:1 user:2

1) "1"

2) "3"

3) "4"

127.0.0.1:6379> sinter user:1 user:3

1) "1"

2) "3"

 

 

并集 sunion

del user:1 user:2 user:3 user:4

sadd user:1 1 2 3 4     # user:1 = {1,2,3,4}

sadd user:2 1 3 4 5     # user:2 = {1,3,4,5}

sadd user:3 1 3 5 6     # user:3 = {1,3,5,6}

sadd user:4 2 3 4       # user:4 = {2,3,4}

 

# 并集

127.0.0.1:6379> sunion user:1 user:2 user:4

1) "1"

2) "2"

3) "3"

4) "4"

5) "5"

 

 

 

差集

del user:1 user:2 user:3 user:4

sadd user:1 1 2 3 4     # user:1 = {1,2,3,4}

sadd user:2 1 3 4 5     # user:2 = {1,3,4,5}

sadd user:3 1 3 5 6     # user:3 = {1,3,5,6}

sadd user:4 2 3 4       # user:4 = {2,3,4}

 

# 差集

127.0.0.1:6379> sdiff user:1 user:3

1) "2"

2) "4"

127.0.0.1:6379> sdiff user:2 user:3

1) "4"

 

127.0.0.1:6379> sdiff user:3 user:2

1) "6"  

练习

127.0.0.1:6379> sadd snames liubei guanyu zhangfei

(integer) 3

127.0.0.1:6379> smembers snames

1) "liubei"

2) "guanyu"

3) "zhangfei"

127.0.0.1:6379> scard snames

(integer) 3

127.0.0.1:6379> spop snames

"guanyu"

127.0.0.1:6379> srem snames liubei

(integer) 1

127.0.0.1:6379> smembers snames

1) "zhangfei"

下午08 zset(5) 去重 权重

zadd

redis值为zset的操作方法

sadd key val1 val2 ...

127.0.0.1:6379> zadd achievements 61 xiaoming 62 xiaohong 83 xiaobai 78 xiaohei 87 xiaohui 99 xiaolan

zincrby  增加权重值

zincrby key score member

127.0.0.1:6379> zincrby achievements 100 xiaoming

zcard 计数

127.0.0.1:6379> zcard achievements

zscore key member 获取某一成员对应的权重

127.0.0.1:6379> zscore achievements xiaoming

 

zrank 排名

zrank key member

127.0.0.1:6379> zcard achievements

6

# score 从小到大的排名

127.0.0.1:6379> zrank achievements xiaoming

5

# score 从大到小的排名

127.0.0.1:6379> zrevrank achievements xiaoming

 

 

 

 

zcount achievements 0 70 区间计数

zcount key min max

127.0.0.1:6379> zcount achievements 0 70

 

zrangebyscore 获取user中60-70之间的数据

127.0.0.1:6379> zrangebyscore achievements 60 70

zrange

127.0.0.1:6379> zrange achievements 0 -1

 

删除

zrem 按元素内容删除

zrem key member1 memeber2 memer3 ...

127.0.0.1:6379> zrem achievements xiaoming

zpopmin 按排名删除

127.0.0.1:6379> zpopmin achievements 2

 

 

zpopmax

# 删除指定数量的成员,从最低score开始删除

zpopmin key [count]

# 删除指定数量的成员,从最高score开始删除

zpopmax key [count]

 

 

常用业务场景

使用场景如下:

字符串string: 用于保存一些项目中的普通数据,只要键值对的都可以保存

例如,保存 session/jwt,定时记录状态,倒计时、验证码、防灌水答案

哈希hash:用于保存项目中的一些结构体/map类型数据,但是不能保存多维结构

例如,商城的购物车,文章信息,json结构数据

列表list:用于保存项目中的列表/切片数据,但是也不能保存多维结构

例如,消息队列,秒杀系统,排队,

无序集合set: 用于保存项目中的一些不能重复的数据,可以用于过滤

例如,候选人名单, 作者名单,

有序集合zset:用于保存项目中一些不能重复,但是需要进行排序的数据

例如:分数排行榜, 海选人排行榜,热搜排行,

 

redis使用场景

数据缓存、

分布式数据共享、

分布式锁、

计数器、

限流、

位统计(用户打卡、签到)、

购物车、

消息队列、

抽奖奖品池、

排行榜单(搜索排名)、

推荐、用户关系记录[收藏、点赞、关注、好友、拉黑]

 

 

 

下午9 redis的发布订阅

 

 

 

 

 

开放封闭原则

 

 

 

 

发布订阅

redis: 消息队列(发布订阅)

订阅 subscribe

发布 publish

 

 

消费者

C:\Users\zhu>redis-cli

127.0.0.1:6379> subscribe msg

Reading messages... (press Ctrl-C to quit)

1) "subscribe"

2) "msg"

3) (integer) 1

 

 

 

再开消费者

C:\Users\zhu>redis-cli

127.0.0.1:6379> subscribe msg

Reading messages... (press Ctrl-C to quit)

1) "subscribe"

2) "msg"

3) (integer) 1

 

发布者

127.0.0.1:6379> publish msg hellmsg

(integer) 2

 

 

下午10 go操作redis

基本连接

package main

import (
   "fmt"
   "github.com/garyburd/redigo/redis"
)

func main() {
   conn,err:=redis.Dial(
      "tcp",
      "127.0.0.1:6379",
      redis.DialPassword(""),
      redis.DialDatabase(0),
   )
   if err!=nil{
      fmt.Println("conn redis error:",err)
      return
   }
   fmt.Println(conn)
   defer conn.Close()
}

 

 

 

 

hash操作

 

package main

import (
   "fmt"
   "github.com/garyburd/redigo/redis"
)

func main(){
   conn,err:=redis.Dial("tcp","127.0.0.1:6379")
   if err!=nil{
      fmt.Println("connect redis error:",err)
      return
   }
   defer conn.Close()
   // 创建hash
   _,err=conn.Do("HSET","student1","username","xiaoming","age",18)
   if err!=nil{
      fmt.Println("redis mset error:",err)
   }

   //获取全部键值对
   res,err:=redis.StringMap(conn.Do("HGETALL","student1"))
   if err!=nil{
      fmt.Println("redis HGETALL err:",err)
   }else{
      fmt.Printf("res=%v \n",res)
      for i,v := range res{
         fmt.Println(i,v)
      }
   }
   // 提取成员
   age,err:=redis.Int64(conn.Do("HGET","student1","age"))
   if err!=nil{
      fmt.Println("redis HGET err:",err)
   }else{
      fmt.Printf("age=%v\n",age)
   }
}

 

 

获取列表

package main

import (
   "fmt"
   "github.com/garyburd/redigo/redis"
   "reflect"
)

func main() {
   conn,err:=redis.Dial("tcp","127.0.0.1:6379")
   if err!=nil{
      fmt.Println("conn redis error:",err)
      return
   }
   defer conn.Close()

   // 创建列表
   _,err=conn.Do("RPUSH","brother","liubei","guanyu","zhangfei")
   if err!=nil{
      fmt.Printf("redis rpush err:",err)
   }

   //末位列出
   item, err := redis.String(conn.Do("RPOP", "brother"))
   if err != nil {
      fmt.Println("redis POP error:", err)
   } else {
      res_type := reflect.TypeOf(item)
      fmt.Printf("res[%s]: %s \n", res_type, item)
   }

   // 获取列表

   res,err:=redis.ByteSlices(conn.Do("LRANGE","brother",0,-1))
   if err != nil {
      fmt.Printf("redis pop error:",err)
   }else{
      fmt.Printf("%s\n",res)
   }
}

 



这篇关于Day15 Redis基础的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程