# 1. Redis相关命令及使用
## 1.1 string
格式:(key:value)
### SET key value
添加一个key value 值(set 键 值)
```shell
127.0.0.1:6379> set user:1001 {name:xiaoMing,password:123456,number:1001}
OK
127.0.0.1:6379> set name xiaoMing
OK
127.0.0.1:6379> set name xiaoMing
OK
127.0.0.1:6379> set name xiaoMing
OK
```
#### 场景: 保存验证码(自带过期时间)
```shell
127.0.0.1:6379> set sms:13567890000 5763 ex 90
```
### GET key
根据一个key得到一个value值(get 键)
```shell
"{name:xiaoMing,password:123456,number:1001}"
127.0.0.1:6379> get name
"xiaoMing"
127.0.0.1:6379> get password
"123456"
127.0.0.1:6379> get number
"123456"
127.0.0.1:6379> get sms:1356789000
```
### GETRANGE key start end
截取某一个字符串指定范围内容,类似java中的subString方法(GETRANGE 键 起始下标 结束下标)
既:0 代表左边第一个元素的下标,-1 代表右边第一个元素的下标
```shell
127.0.0.1:6379> GETRANGE number 0 -1
"123456"
127.0.0.1:6379> GETRANGE number 0 -1
"123456"
127.0.0.1:6379> GETRANGE number 0 1
"12"
127.0.0.1:6379> GETRANGE number 1 -1
"23456"
127.0.0.1:6379> GETRANGE number 5 -1
"6"
127.0.0.1:6379> GETRANGE number -2 -1
"56"
127.0.0.1:6379> GETRANGE number -2 -2
"5"
127.0.0.1:6379> GETRANGE number -2 -5
""
127.0.0.1:6379> GETRANGE number 0 -5
"12"
```
### GETSET key value
替换一个key中的value值,返回被替换的值
```shell
127.0.0.1:6379> GETSET number 123
"123456"
127.0.0.1:6379> get number
"123"
```
### MSET key value [key value ...]
批量添加key value 值
```shell
127.0.0.1:6379> MSET name1 xiaoMing name2 xiaoHong name3 123
OK
```
### MSET key value [key value ...]
批量获取多个key的value值
```shell
127.0.0.1:6379> MGET name1 name2 name3
1) "xiaoMing"
2) "xiaoHong"
3) "123"
```
### DEL key [key ...]
批量删除多个key
```shell
127.0.0.1:6379> del name1 name2
(integer) 2
```
### INCR key
递增,根据一个key中的整数数字+1,非整数数字会报错
```shell
127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number
(integer) 2
127.0.0.1:6379> incr number
127.0.0.1:6379> set name zhan
OK
127.0.0.1:6379> incr name
(error) ERR value is not an integer or out of range
```
#### 场景:生成订单编号
集群服务器中,订单的生成通过一台redis的服务器保证唯一性
### INCRBY key increment
增加指定的整数,根据一个key中的整数数字+指定整数,非整数数字会报错
```shell
127.0.0.1:6379> incrby number 10
(integer) 13
127.0.0.1:6379> incrby name 10
(error) ERR value is not an integer or out of range
```
### DECR key
根据一个key中的整数数字-1,非整数数字会报错
```shell
127.0.0.1:6379> DECR number
(integer) 12
127.0.0.1:6379> DECR name
(error) ERR value is not an integer or out of range
```
### DECRBY key decrement
根据一个key中的整数数字-指定整数,非整数数字会报错
```shell
127.0.0.1:6379> DECRBY number 10
(integer) 2
127.0.0.1:6379> DECRBY name 10
(error) ERR value is not an integer or out of range
```
### APPEND key value
根据一个key中的内容追加指定内容,如果key不存在,则生成一条新的k-v记录,返回增加后的value的长度
```shell
127.0.0.1:6379> APPEND name 123
(integer) 7
127.0.0.1:6379> get name
"zhan123"
127.0.0.1:6379> APPEND name xiao
(integer) 11
127.0.0.1:6379> get name
"zhan123xiao"
```
### STRLEN key
返回key对应值的长度,如果键不存在则返回0
```shell
127.0.0.1:6379> STRLEN name
(integer) 11
127.0.0.1:6379> STRLEN name12
(integer) 0
```
## 1.2 hset
格式:(key column1:value1 column2:value2 column3:value3)
类似于java 当中的 对象 属性1:值1 属性2:值2 属性3:值3
### HSET key field value
set一个对象,并set对应的属性和值
```shell
127.0.0.1:6379> hset user:1002 name xiaoMing
(integer) 1
127.0.0.1:6379> hset user:1002 password 123456
(integer) 1
127.0.0.1:6379> hset user:1002 number 1002
(integer) 1
```
### HMSET key field value [field value ...]
为对象set多个属性、值(一个属性对应一个值)
```shell
127.0.0.1:6379> hmset user:1003 name xiaoHui password 123456 number 1003
OK
```
### HSETNX key field value
如果对象或者对象中的属性不存在时,才会执行set对象或者set对象中的属性和值
```shell
127.0.0.1:6379> HSETNX user:1004 name xiaoHe
(integer) 1
127.0.0.1:6379> HSETNX user:1004 name xiaoTian
(integer) 0
```
### HGET key field
获取一个字段值
```shell
127.0.0.1:6379> hget user:1004 name
"xiaoHe"
127.0.0.1:6379> hget user:1003 name
"xiaoHui"
```
### HMGET key field [field ...]
获取多个字段值
```shell
127.0.0.1:6379> hmget user:1002 name password number
1) "xiaoMing"
2) "123456"
3) "1002"
```
### HGETALL key
获取对象中所有属性和值
```shell
127.0.0.1:6379> HGETALL user:1002
1) "name"
2) "xiaoMing"
3) "password"
4) "123456"
5) "number"
6) "1002"
```
### 7)HDEL key field [field ...]
删除一个或多个字段,返回结果显示被删除的个数,如果没有对应字段,返回0
```shell
127.0.0.1:6379> hdel user:1004 name
(integer) 1
127.0.0.1:6379> hdel user:1004 name
(integer) 0
```
### 8)HINCRBY key field increment
增加指定的大小,返回增加后的结果值
注意: 字段的类型也必须是数值型, 否则会报错
```shell
127.0.0.1:6379> HINCRBY user:1002 number 10
(integer) 1012
127.0.0.1:6379> HINCRBY user:1002 name 10
(error) ERR hash value is not an integer
```
### 9)HEXISTS key field
判断一个对象或者对象中某一个字段是否存在
```shell
127.0.0.1:6379> HEXISTS user:1002 name
(integer) 1
127.0.0.1:6379> HEXISTS user:1002 name1
(integer) 0
```
### HKEYS key
获取对象中的所有字段名
```shell
127.0.0.1:6379> HKEYS user:1002
1) "name"
2) "password"
3) "number"
127.0.0.1:6379> HKEYS user:1004
1) "name"
```
### HVALS key
获取对象中的所有字段值
```shell
127.0.0.1:6379> HVALS user:1002
1) "xiaoMing"
2) "123456"
3) "1012"
127.0.0.1:6379> HVALS user:1004
1) "xiaoHong"
```
### HLEN key
获取对象中的字段个数
```shell
127.0.0.1:6379> HLEN user:1002
(integer) 3
127.0.0.1:6379> HLEN user:1004
(integer) 1
```
## 1.3 list
特点:有序、可重复、依靠下标作为索引
底层: linkedlist 双向链表
使用场景: 粉丝列表 关注列表 好友列表 / 对位置操作
Redis的list是采用来链表来存储的,所以对于redis的list数据类型的操作,是操作list的两端数据来操作的。
### LPUSH key value [value ...]
向列表左边增加元素
```shell
127.0.0.1:6379> LPUSH list:a 1
(integer) 1
127.0.0.1:6379> LPUSH list:a 2
(integer) 2
127.0.0.1:6379> LPUSH list:a 3
(integer) 3
```
### LPUSHX key value [value ...]
如果列表存在,则向列表左边增加元素
```shell
127.0.0.1:6379> LPUSHX list:a 4
(integer) 4
127.0.0.1:6379> LPUSHX list 1
(integer) 0
```
### RPUSH key value [value ...]
向列表右边增加元素
```shell
127.0.0.1:6379> rpush list:a 0
(integer) 5
127.0.0.1:6379> rpush list:a -1
(integer) 6
127.0.0.1:6379> rpush list:a -2 -3
(integer) 8
```
### RPUSHX key value [value ...]
如果列表存在,则向列表右边增加元素
```shell
127.0.0.1:6379> rpushx list:a -4
(integer) 9
127.0.0.1:6379> rpushx list -4
(integer) 0
```
### LRANGE key start stop
LRANGE命令是列表类型最常用的命令之一,获取列表中的某一片段,将返回start、stop之间的所有元素(包含两端的元素),索引从0开始。索引可以是负数,如:"-1"代表最后边的一个元素。
```shell
127.0.0.1:6379> LRANGE list:a 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "0"
6) "-1"
7) "-2"
8) "-3"
9) "-4"
127.0.0.1:6379> LRANGE list:a 0 1
1) "4"
2) "3"
127.0.0.1:6379> LRANGE list:a 5 -1
1) "-1"
2) "-2"
3) "-3"
4) "-4"
```
### LPOP key || RPOP key
从列表两端移除元素,POP命令从列表左边或者右边弹出一个元素,会分两步完成:
第一步是将列表左边或者右边的元素从列表中移除
第二步是返回被移除的元素值。
```shell
127.0.0.1:6379> LPOP list:a
"4"
127.0.0.1:6379> LRANGE list:a 0 -1
1) "3"
2) "2"
3) "1"
4) "0"
5) "-1"
6) "-2"
7) "-3"
8) "-4"
127.0.0.1:6379> RPOP list:a
"-4"
127.0.0.1:6379> LRANGE list:a 0 -1
1) "3"
2) "2"
3) "1"
4) "0"
5) "-1"
6) "-2"
7) "-3"
```
### BLPOP key || BRPOP key
和LPOP||RPOP功能类似,但是此种方式是阻塞的,并且返回的是一个key和value,在传递参数时,多增加了一个时间,代表阻塞等待的时间
PS:此种方式可以同时取多个list中的数据,以谁先取到为结束条件,如果都没取到,则被阻塞,直到超时,返回nil
```shell
127.0.0.1:6379> BLPOP list:a 2
1) "list:a"
2) "3"
127.0.0.1:6379> BLPOP list 10
(nil)
(10.07s)
127.0.0.1:6379> BLPOP list 10
1) "list"
2) "3"
(2.07s)
127.0.0.1:6379> LPUSH list 1 2 3
(integer) 3(此部分为绿色部分代码执行后2s时在另一个连接窗口上执行的代码)
```
### LLEN key
获取列表中元素的个数
```shell
127.0.0.1:6379> llen list:a
(integer) 6
127.0.0.1:6379> llen list
(integer) 2
```
### LREM key count value
删除列表中指定的值,LREM命令会删除列表中前count个值为value的元素,返回实际删除的元素个数。根据count值的不同,该命令的执行方式会有所不同:
当count>0时,LREM会从列表左边开始删除指定个数。
当count<0时,LREM会从列表后边开始删除指定个数。
当count=0时,LREM删除所有值为value的元素。
注意:count代表总共删除指定的元素几个
```shell
127.0.0.1:6379> LRANGE list:b 0 -1
1) "1"
2) "2"
3) "1"
4) "2"
5) "1"
6) "2"
7) "1"
127.0.0.1:6379> LREM list:b -1 1
(integer) 1
127.0.0.1:6379> LRANGE list:b 0 -1
1) "1"
2) "2"
3) "1"
4) "2"
5) "1"
6) "2"
127.0.0.1:6379> LREM list:b -2 2
(integer) 2
127.0.0.1:6379> LRANGE list:b 0 -1
1) "1"
2) "2"
3) "1"
4) "1"
127.0.0.1:6379> LREM list:b 1 1
(integer) 1
127.0.0.1:6379> LRANGE list:b 0 -1
1) "2"
2) "1"
3) "1"
127.0.0.1:6379> LREM list:b 0 1
(integer) 2
127.0.0.1:6379> LRANGE list:b 0 -1
1) "2"
```
### LINDEX key index
获得指定索引的元素值
```shell
127.0.0.1:6379> LRANGE list:a 0 -1
1) "2"
2) "1"
3) "0"
4) "-1"
5) "-2"
6) "-3"
127.0.0.1:6379> LINDEX list:a 0
"2"
127.0.0.1:6379> LINDEX list:a 1
"1"
127.0.0.1:6379> LINDEX list:a 2
"0"
```
### LSET key index value
设置指定索引的元素值(修改, 将原值覆盖)
```shell
127.0.0.1:6379> LINDEX list:a 2
"0"
127.0.0.1:6379> LSET list:a 2 9
OK
127.0.0.1:6379> LINDEX list:a 2
"9"
```
### LTRIM key start stop
只保留当前list中的start -stop下标内容,其余都删除
```shell
127.0.0.1:6379> LRANGE list:a 0 -1
1) "2"
2) "1"
3) "9"
4) "-1"
5) "-2"
6) "-3"
127.0.0.1:6379> LTRIM list:a 0 3
OK
127.0.0.1:6379> LRANGE list:a 0 -1
1) "2"
2) "1"
3) "9"
4) "-1"
```
### LINSERT key BEFORE|AFTER pivot value
向列表中插入元素,该命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面。
```shell
127.0.0.1:6379> LRANGE list:a 0 -1
1) "2"
2) "1"
3) "9"
4) "-1"
127.0.0.1:6379> LINSERT list:a before -1 0
(integer) 5
127.0.0.1:6379> LRANGE list:a 0 -1
1) "2"
2) "1"
3) "9"
4) "0"
5) "-1"
127.0.0.1:6379> LINSERT list:a after -1 0
(integer) 6
127.0.0.1:6379> LRANGE list:a 0 -1
1) "2"
2) "1"
3) "9"
4) "0"
5) "-1"
6) "0"
```
### RPOPLPUSH source destination
将最后位的一个元素从一个列表转移到另一个列表中
```shell
127.0.0.1:6379> LRANGE list:a 0 -1
1) "2"
2) "1"
3) "9"
4) "0"
5) "-1"
127.0.0.1:6379> LRANGE list 0 -1
1) "0"
2) "2"
3) "1"
127.0.0.1:6379> RPOPLPUSH list:a list
"-1"
127.0.0.1:6379> LRANGE list:a 0 -1
1) "2"
2) "1"
3) "9"
4) "0"
127.0.0.1:6379> LRANGE list 0 -1
1) "-1"
2) "0"
3) "2"
4) "1"
```
## 1.4 set
无序、不可重复、依靠键作为索引
### SADD key member [member ...]
增加元素
```shell
127.0.0.1:6379> sadd setA 1 2 3 4
(integer) 4
127.0.0.1:6379> sadd setB 1 2 3 4
(integer) 4
```
### SMEMBERS key
显示集合成员
```shell
127.0.0.1:6379> SMEMBERS setA
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> SMEMBERS setB
1) "1"
2) "2"
3) "3"
4) "4"
```
### SREM key member [member ...]
删除一个或者多个元素
```shell
127.0.0.1:6379> SREM setA 1 2
(integer) 2
127.0.0.1:6379> SMEMBERS setA
1) "3"
2) "4"
```
### SISMEMBER key member
判断元素是否在集合中
```shell
127.0.0.1:6379> SMEMBERS setA
1) "3"
2) "4"
127.0.0.1:6379> SISMEMBER setA 1
(integer) 0
127.0.0.1:6379> SISMEMBER setA 3
(integer) 1
```
### SDIFF key [key ...]
集合的差集运算A-B,属于A并且不属于B的元素构成的集合。
场景: 判断非共同好友
```shell
127.0.0.1:6379> SMEMBERS setA
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> SMEMBERS setB
1) "2"
2) "3"
3) "4"
4) "5"
127.0.0.1:6379> SDIFF setA setB
1) "1"
127.0.0.1:6379> SDIFF setB setA
1) "5"
```
### SINTER key [key ...]
集合的交集运算A ∩ B,属于A且属于B的元素构成的集合。
场景:判断共有的用户
```shell
127.0.0.1:6379> SINTER setA setB
1) "2"
2) "3"
3) "4"
```
### SUNION key [key ...]
集合的并集运算A ∪ B,属于A或者属于B的元素构成的集合
场景: 本地通讯录和云备份通讯录合并
```shell
127.0.0.1:6379> SUNION setA setB
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
```
### SCARD key
获得集合中元素的个数
```shell
127.0.0.1:6379> SCARD setA
(integer) 4
127.0.0.1:6379> SCARD setB
(integer) 4
```
### SPOP key [count]
从集合中弹出一个元素(或多个)
注意:由于集合是无序的,所有SPOP命令会从集合中随机选择一个元素弹出
场景: 网络抽奖
```shell
127.0.0.1:6379> SPOP setA
"3"
127.0.0.1:6379> SPOP setA
"2"
127.0.0.1:6379> SPOP setA
"4"
127.0.0.1:6379> SPOP setA
"1"
127.0.0.1:6379> SPOP setA
(nil)
127.0.0.1:6379> SPOP setB 2
1) "2"
2) "4"
127.0.0.1:6379> SPOP setB 2
1) "3"
2) "5"
```
## 1.5 sortedset
有序、不可重复
sortedset又叫zset,是有序集合,可排序的,但是唯一。
sortedset和set的不同之处,是会给set中的元素添加一个分数,然后通过这个分数进行排序。
### ZADD key score member [score member ...]
增加元素,向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。返回值是新加入到集合中的元素个数,不包含之前已经存在的元素。
```shell
127.0.0.1:6379> zadd hua 10 meigui
(integer) 1
127.0.0.1:6379> zadd hua 11 juhua
(integer) 1
127.0.0.1:6379> zadd hua 11 juhua 15 baihe
(integer) 1
127.0.0.1:6379> zadd hua 11 juhua 15 baihe
(integer) 0
127.0.0.1:6379> zadd hua 9 molo 20 hehua
(integer) 2
```
### ZSCORE key member
获取元素的分数
```shell
127.0.0.1:6379> ZSCORE hua meigui
"10"
127.0.0.1:6379> ZSCORE hua hehua
"20"
```
### ZREM key member [member ...]
删除元素,移除有序集key中的一个或多个成员,不存在的成员将被忽略。当key存在但不是有序集类型时,返回一个错误。
```shell
127.0.0.1:6379> ZREM hua hehua
(integer) 1
127.0.0.1:6379> ZREM hua hehua
(integer) 0
127.0.0.1:6379> ZREM hua hehua meigui
(integer) 1
```
### ZRANGE key start stop [WITHSCORES]
获得排名在某个范围的元素列表,按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
```shell
127.0.0.1:6379> ZRANGE hua 0 1
1) "molo"
2) "juhua"
127.0.0.1:6379> ZRANGE hua 0 2
1) "molo"
2) "juhua"
3) "baihe"
```
### ZREVRANGE key start stop [WITHSCORES]
按照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素),如果需要获得元素的分数的可以在命令尾部加上WITHSCORES参数,例如下面的效果:
```shell
127.0.0.1:6379> ZREVRANGE hua 0 1
1) "baihe"
2) "juhua"
127.0.0.1:6379> ZREVRANGE hua 0 2
1) "baihe"
2) "juhua"
3) "molo"
127.0.0.1:6379> ZREVRANGE hua 0 2 WITHSCORES
1) "baihe"
2) "15"
3) "juhua"
4) "11"
5) "molo"
6) "9"
```
### ZRANK key member
获取元素的排名从小到大
```shell
127.0.0.1:6379> ZRANK hua molo
(integer) 0
127.0.0.1:6379> ZRANK hua juhua
(integer) 1
127.0.0.1:6379> ZRANK hua baihe
(integer) 2
```
### ZREVRANK key member
从大到小
```shell
127.0.0.1:6379> ZREVRANK hua molo
(integer) 2
127.0.0.1:6379> ZREVRANK hua baihe
(integer) 0
127.0.0.1:6379> ZREVRANK hua juhua
(integer) 1
```
### ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
获得指定分数范围的元素
```shell
127.0.0.1:6379> ZRANGEBYSCORE hua 0 100
1) "molo"
2) "juhua"
3) "baihe"
127.0.0.1:6379> ZRANGEBYSCORE hua 0 10
1) "molo"
```
### ZINCRBY key increment member
增加某个元素的分数,返回值是更改后的分数
```shell
127.0.0.1:6379> ZINCRBY hua 10 molo
"19"
127.0.0.1:6379> ZINCRBY hua 10 baihe
"25"
127.0.0.1:6379> ZINCRBY hua 15 juhua
"26"
```
### ZCARD key
获得集合中元素的数量
```shell
127.0.0.1:6379> ZCARD hua
(integer) 3
127.0.0.1:6379> zadd shui 10 wahaha 20 maidong
(integer) 2
127.0.0.1:6379> ZCARD shui
(integer) 2
```
### ZCOUNT key min max
获得指定分数范围内的元素个数
```shell
127.0.0.1:6379> ZCOUNT hua 0 10
(integer) 0
127.0.0.1:6379> ZCOUNT hua 0 20
(integer) 1
127.0.0.1:6379> ZCOUNT hua 0 30
(integer) 3
```
### ZREMRANGEBYRANK key start stop
按照排名范围删除元素
```shell
127.0.0.1:6379> ZREMRANGEBYRANK hua 0 10
(integer) 3
127.0.0.1:6379> ZREMRANGEBYRANK hua 0 10
(integer) 0
```
### ZREMRANGEBYSCORE key min max
按照分数范围删除元素
```shell
127.0.0.1:6379> ZREMRANGEBYSCORE shui 0 10
(integer) 1
127.0.0.1:6379> ZREMRANGEBYSCORE shui 0 10
(integer) 0
127.0.0.1:6379> ZREMRANGEBYSCORE shui 0 20
(integer) 1
127.0.0.1:6379> ZREMRANGEBYSCORE shui 0 20
(integer) 0
```
# 2. Redis相关应用场景
## 2.1 计数器(原子性、单线程、微秒级别)
商品浏览量,视频播放数常用,关键命令: incr,decr
```shell
127.0.0.1:6379> incr video:sanshengsanshi
(integer) 1
127.0.0.1:6379> incr video: sanshengsanshi
(integer) 2
127.0.0.1:6379> incr video: sanshengsanshi
(integer) 3
127.0.0.1:6379> incr video: sanshengsanshi
(integer) 4
127.0.0.1:6379> decr video: sanshengsanshi
(integer) 3
```
## 2.2 缓存 缓存商城首页数据
使用zset(分数用评论数/上架时间/价格等) , value用商品id
使用string/hash 存储商品信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XmHP2fFR-1626654480870)(Redis%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E4%BB%8B%E7%BB%8D.assets/image-20210701212119791.png)]
使用zset存放所有推荐商品的id,根据价格作为排序依据
```shell
127.0.0.1:6379> zadd hotitem:price 1799 1001 1499 1002 626 1003 8999 1004 4999 1005
(integer) 5
```
使用string(json)存放商品信息
```shell
127.0.0.1:6379> mset 1001 {name:skyworth,intr:tel,price:3499,img:1001.png}
OK
```
使用hash存放商品信息
```shell
127.0.0.1:6379> HSET item:1001 name skyworth
(integer) 1
127.0.0.1:6379> HSET item:1001 price 1799
(integer) 1
```
显示商品列表
```shell
127.0.0.1:6379> ZREVRANGE hotitem:price 0 4
1) "1004"
2) "1005"
3) "1001"
4) "1002"
5) "1003"
```
根据结果进一步查询商品具体信息
```shell
127.0.0.1:6379> get 1001
"{name:skyworth,intr:tel,price:3499,img:1001.png}"
```
## 2.3 购物车:hset hdel hlen hincrby hgetall
key: 用户id
field: 商品id
商品数量: value
使用hset实现购物车场景
操作: 编号1999的用户购物车添加商品编号为1001和1004的商品
```shell
127.0.0.1:6379> hset cart:1999 1001 1
(integer) 1
```
添加商品: hset cart:1999 1004 1
```shell
127.0.0.1:6379> hset cart:1999 1004 1
(integer) 1
```
添加数量: hincrby cart: 1999 1004 1
```shell
127.0.0.1:6379> HINCRBY cart:1999 1004 1
(integer) 2
```
商品总数: hlen cart:1999
```shell
127.0.0.1:6379> hlen cart:1999
(integer) 2
```
获取购物车所有商品(全选) hgetall cart:1999
```shell
127.0.0.1:6379> HGETALL cart:1999
1) "1004"
2) "2"
3) "1001"
4) "1"
```
删除商品: hdel cart:1999 1001
```shell
127.0.0.1:6379> HDEL cart:1999 1001
(integer) 1
```
## 2.4 抢购,限购发放优惠券,激活码等
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-giScOFv7-1626654480875)(Redis%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E4%BB%8B%E7%BB%8D.assets/20200525192416499.png)]
解决方案
- 以商家id作为key
- 将参与抢购的商品id作为field
- 将参与抢购的商品数量作为对应的value
- 抢购时使用降值的方式控制产品数量
## 2.5 商品筛选
准备noteBookSet集合
```shell
127.0.0.1:6379> sadd notebookset huawei14 vivo15 apple15 lenovo16 dell17
(integer) 5
```
准备i7Set集合
```shell
127.0.0.1:6379> sadd i7set huawei14 apple15 lenovo16 dell17
(integer) 4
```
准备8gSet集合
```shell
127.0.0.1:6379> sadd 8gset huawei14 lenovo16 dell17
(integer) 3
```
选8g+i7+notebook
```shell
127.0.0.1:6379> SINTER notebookset i7set 8gset
1) "lenovo16"
2) "dell17"
3) "huawei14"
```
## 2.6 存储对象(不常变化): 使用json
字符串型(不方便对数据中的单个属性进行编辑操作)
```shell
127.0.0.1:6379> set hua:1001 {name:hehua,number:1001,price:15}
OK
```
## 2.7 存储对象(频繁变化): hset hincrby
经常改变的使用hash(方便对单个属性进行编辑操作)
```shell
127.0.0.1:6379> hset hehua name hehua
(integer) 1
127.0.0.1:6379> hset hehua number 1001
(integer) 1
127.0.0.1:6379> hset hehua price 15
(integer) 1
```
## 2.8 使用list实现栈/队列/阻塞队列
栈:LPUSH +LPOP -->FILO
先进后出原则:LPUSH从队列左边进入d,c,b,a, LPOP从队列左边出来a,b,c,d
```shell
127.0.0.1:6379> lpush stack 1 2 3 4
(integer) 4
127.0.0.1:6379> LRANGE stack 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
127.0.0.1:6379> lpop stack
"4"
127.0.0.1:6379> lpop stack
"3"
127.0.0.1:6379> lpop stack
"2"
127.0.0.1:6379> lpop stack
"1"
```
队列: LPUSH+RPOP
先进先出原则:LPUSH从队列左边进入d,c,b,a, RPOP从队列右边出d,c,b,a
```shell
127.0.0.1:6379> LPUSH queue a b c d
(integer) 4
127.0.0.1:6379> LRANGE queue 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
127.0.0.1:6379> rpop queue
"a"
127.0.0.1:6379> rpop queue
"b"
127.0.0.1:6379> rpop queue
"c"
127.0.0.1:6379> rpop queue
"d"
```
阻塞队列: LPUSH+BRPOP(b:blocking)
LPUSH+BRPOP是在LPUSH+RPOP的基础上多了阻塞和等待的功能,
```shell
127.0.0.1:6379> BRPOP queue 2
(nil)
(2.04s)
```
因为队列不存在,所以消耗完了等待时间2s,如果队列存在,则会立即执行弹出操作
## 2.9 实现微博消息和公众号消息(MQ : Message Queue)
redis实现消息队列
老毕(id:9527) 关注了 周杰伦和周星驰
周杰伦发微博, 消息id:1001314
lpush msg:9527 1001314
周星驰发微博,消息id: 1000520
lpush msg:9527 1000520
```shell
127.0.0.1:6379> lpush msg:9527 1001234
(integer) 1
127.0.0.1:6379> lpush msg:9527 1001314
(integer) 2
127.0.0.1:6379> lpush msg:9527 1001312
(integer) 3
127.0.0.1:6379> lpush msg:9527 1001315
(integer) 4
```
查询最新微博消息(15条)
lrange msg:9527 0 14
```shell
127.0.0.1:6379> LRANGE msg:9527 0 14
1) "1001315"
2) "1001312"
3) "1001314"
4) "1001234"
```
## 2.10 高热度数据访问加速
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1j3OGsa9-1626654480880)(Redis%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E4%BB%8B%E7%BB%8D.assets/20200525145645408.png)]
解决方案1: 大V用户设定用户信息,以用户主键和属性值作为key,后台设定时间定时刷新即可
```shell
127.0.0.1:6379> set user:id:5765898790:focuss:3050
OK
127.0.0.1:6379> set user:id:5765898790:fans:117492300
OK
127.0.0.1:6379> set user:id:5765898790:blogs:117744
OK
```
解决方案2: 在Redis中以json格式存储大V用户,定时刷新
```shell
127.0.0.1:6379> set user: id :5765898790 {id:5765898790,focuss:3050,fans:117492300,blogs:117744}
OK
```
-
## 2.11 朋友圈点赞
微信朋友圈点赞,要求按照点赞顺序显示点赞好友信息。
如果取消点赞,移除对应好友信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IeQ6rMNZ-1626654480883)(Redis%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E4%BB%8B%E7%BB%8D.assets/20200525200029413.png)]
```shell
127.0.0.1:6379> lrange daqiao 0 -1
1) "zhangfei"
2) "liubei"
3) "guanyu"
4) "zhaoyun"
5) "caocao"
6) "xiaoqiao"
7) "zhangfei"
127.0.0.1:6379> lrem daqiao 1 liubei
(integer) 1
127.0.0.1:6379> lrange daqiao 0 -1
1) "zhangfei"
2) "guanyu"
3) "zhaoyun"
4) "caocao"
5) "xiaoqiao"
6) "zhangfei"
127.0.0.1:6379> lrem daqiao 1 zhangfei
(integer) 1
127.0.0.1:6379> lrange daqiao 0 -1
1) "guanyu"
2) "zhaoyun"
3) "caocao"
4) "xiaoqiao"
5) "zhangfei"
```
## 2.12 使用set实现关注模型
我(9527)关注的
```shell
127.0.0.1:6379> sadd focus:9527 zhangmeng hanyun liuyifei xuqing
(integer) 4
```
他(9528)关注的
```shell
127.0.0.1:6379> sadd focus:9528 hanyun liuyifei xuqing xiaofeng
(integer) 4
```
她(9529)关注的
```shell
127.0.0.1:6379> sadd focus:9529 zhangmeng hanyun xuqing
(integer) 3
```
我(9527)和他(9528)共同关注的
```shell
127.0.0.1:6379> SINTER focus:9527 focus:9528
1) "liuyifei"
2) "hanyun"
3) "xuqing"
```
我(9527)关注的人(张萌)她(9529)也关注了,但是他(9528)没有关注
```shell
127.0.0.1:6379> SISMEMBER focus:9527 zhangmeng
(integer) 1
127.0.0.1:6379> SISMEMBER focus:9529 zhangmeng
(integer) 1
127.0.0.1:6379> SISMEMBER focus:9528 zhangmeng
(integer) 0
```
可能认识的人(我(9527)和他(9528)是好友,那个人是他(9528)的好友但不是我(9527)的好友,这个人就可能是我(9527)的好友)
```shell
127.0.0.1:6379> SDIFF focus:9528 focus:9527
1) "xiaofeng"
```
## 2.13 分布式锁(购物中商品的库存)
超买超卖
1.锁住当前库存
2.库存-1(核心业务)
3.释放锁
下订单操作进行时,锁住资源
```shell
127.0.0.1:6379> SETNX item:001 true
(integer) 1
```
其他人不可进行下单操作
```shell
127.0.0.1:6379> SETNX item:001 true
(integer) 0
```
下单操作完成释放锁
```shell
127.0.0.1:6379> del item:001
(integer) 1
```
其他人可继续进行下单操作
```shell
127.0.0.1:6379> SETNX item:001 true
(integer) 1
```
## 2.14 网络抽奖
SRANDMEMBER lot 1 每次抽取1人(该用户不会从备选人中排除,可重复参与抽奖)
```shell
127.0.0.1:6379> sadd lot:9527 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379> SRANDMEMBER lot:9527 1
1) "7"
127.0.0.1:6379> SRANDMEMBER lot:9527 1
1) "3"
127.0.0.1:6379> SRANDMEMBER lot:9527 1
1) "4"
127.0.0.1:6379> SRANDMEMBER lot:9527 1
1) "3"
127.0.0.1:6379> SRANDMEMBER lot:9527 1
1) "3"
127.0.0.1:6379> SMEMBERS lot:9527
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
```
SPOP lot 1 每次抽取1人(该用户就从备选人中排除,不可重复参与抽奖)
```shell
127.0.0.1:6379> SPOP lot:9527 1
1) "2"
127.0.0.1:6379> SPOP lot:9527 1
1) "6"
127.0.0.1:6379> SMEMBERS lot:9527
1) "1"
2) "3"
3) "4"
4) "5"
5) "7"
6) "8"
7) "9"
```
## 2.15 热搜排行
准备19号的热搜新闻
```shell
127.0.0.1:6379> zadd hotnews:20202819 1 1001
(integer) 1
127.0.0.1:6379> zadd hotnews:20202819 1 1002
(integer) 1
127.0.0.1:6379> zadd hotnews:20202819 1 1003
(integer) 1
127.0.0.1:6379> zadd hotnews:20202819 1 1004
(integer) 1
127.0.0.1:6379> zadd hotnews:20202819 1 1005
(integer) 1
```
19号热搜点击之后在19号+1
```shell
127.0.0.1:6379> ZINCRBY hotnews:20202819 1 1001
"2"
127.0.0.1:6379> ZINCRBY hotnews:20202819 1 1002
"2"
127.0.0.1:6379> ZINCRBY hotnews:20202819 1 1003
"2"
127.0.0.1:6379> ZINCRBY hotnews:20202819 1 1002
"3"
127.0.0.1:6379> ZINCRBY hotnews:20202819 1 1002
"4"
127.0.0.1:6379> ZINCRBY hotnews:20202819 1 1003
"3"
```
查看19号的热搜榜
```shell
127.0.0.1:6379> ZREVRANGE hotnews:20202819 0 -1 withscores
1) "1002"
2) "4"
3) "1003"
4) "3"
5) "1001"
6) "2"
7) "1005"
8) "1"
9) "1004"
10) "1"
```
准备20号的热搜新闻(19号的热搜也可上20号的热搜)
```shell
127.0.0.1:6379> zadd hotnews:20202820 1 1001
(integer) 1
127.0.0.1:6379> zadd hotnews:20202820 1 1002
(integer) 1
127.0.0.1:6379> zadd hotnews:20202820 1 1006
(integer) 1
```
20号热搜点击之后在20号+1
```SHELL
127.0.0.1:6379> ZINCRBY hotnews:20202820 1 1001
"2"
127.0.0.1:6379> ZINCRBY hotnews:20202820 1 1001
"3"
127.0.0.1:6379> ZINCRBY hotnews:20202820 1 1002
"2"
127.0.0.1:6379> ZINCRBY hotnews:20202820 1 1006
"2"
```
查看两日热搜榜
```shell
127.0.0.1:6379> ZUNIONSTORE 2days 2 hotnews:20202819 hotnews:20202820
(integer) 6
127.0.0.1:6379> ZREVRANGE 2days 0 -1 withscores
1) "1002"
2) "6"
3) "1001"
4) "5"
5) "1003"
6) "3"
7) "1006"
8) "2"
9) "1005"
10) "1"
11) "1004"
12) "1"
```
# 3. Java操作Redis-Jedis
### 3.1 导包
```xml
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
```
### 3.2 操作string类型
```java
@SpringBootTest(classes = MainApplication.class)
@RunWith(SpringRunner.class)
public class TestJedis {
//1.准备连接池对象
private JedisPool jedisPool = null;
//2. 准备jedis对象
private Jedis jedis = null;
@Before
public void init(){
// 建立连接
jedisPool=new JedisPool("139.198.183.226",6379);
//jedis客户端
jedis = jedisPool.getResource();
//验证权限
jedis.auth("!123456Bl");
}
@Test
public void testString(){
jedis.set("name","张三丰");
String result = jedis.get("name");
System.out.println(result);
}
@After
public void close(){
jedisPool.close();
}
}
```
==从hash开始省略了@Before和@After, 测试时需加上==
### 3.3 操作hash类型
```java
@Test
public void testHashMap(){
//以key:{field:value} 方式添加
jedis.hset("user","name","zhang");
jedis.hset("user","age","20");
// 以map形式添加:
/* Map map = new HashMap();
map.put("name","zhang");
map.put("age","20");
jedis.hset("user2", map);*/
String userName = jedis.hget("user", "name");
System.out.println(userName);
// 获取全部特征
Map<String, String> userMap = jedis.hgetAll("user");
System.out.println(userMap);
}
```
### 3.4 操作列表类型
```java
@Test
public void testList(){
//操作列表类型
//存储
jedis.lpush("mylist","a","b","c");//从左边存
jedis.rpush("mylist2","a","b","c");//从右边存
//list获取
List<String> mylist = jedis.lrange("mylist", 0, -1);
System.out.println(mylist);
//弹出
String e1 = jedis.lpop("mylist");
System.out.println(e1);//c
String e2 = jedis.rpop("mylist");
System.out.println(e2);//a
}
```
### 3.5 操作集合类型(无序)
```java
@Test
public void testSet(){
//集合 不可重复 无序
jedis.sadd("set","a","c","b");
Set<String> set = jedis.smembers("set");
System.out.println(set);
}
```
### 3.6 操作集合类型(排序)
```java
@Test
public void testZset(){
//set集合 不可重复 排序
jedis.zadd("hotitem",90,"电动牙刷");
jedis.zadd("hotitem",60,"篮球");
jedis.zadd("hotitem",80,"钢笔");
Set<String> set = jedis.zrange("hotitem", 0, -1);
System.out.println(set);
}
```
# 4. Springboot整合redis
## 4.1 添加依赖
```xml
<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
## 4.2 yml中设置redis的连接
添加redis的连接和连接池配置
```yml
spring:
redis:
database: 0
host: 139.198.183.226
port: 6379
password: "!123456Bl"
jedis:
pool:
max-idle: -1 #没有限制 , 最大闲时连接
min-idle: 1000 #最小连接
```
## 4.3 设计配置类(定义序列化器)
```java
@Configuration
public class RedisConfig {
//为了在service中调用redis的操作类, 提前通过@Bean的方式准备好
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 实例化一个RedisTemplate(操作redis中不同数据类型的CRUD)
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
// 给template配置一个factory
template.setConnectionFactory(factory);
//配置序列化器: 针对String和hash采用何种序列化方式(java把数据传给redis时使用何种格式: jdk/string/jackson)
//Jackson序列化器
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(om);
//String序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
// user[name=张三] -> {"user":{"name":"张三"}}
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
```
## 4.4 设计一个测试用例
```java
@SpringBootTest(classes = PortalApp.class)
@RunWith(SpringRunner.class)
public class TestRedisTemplate {
@Resource
private RedisTemplate<String,Object> redisTemplate;
@Test
public void testString(){
// 获取一个针对String类型的操作对象
ValueOperations<String, Object> stringObjectValueOperations = redisTemplate.opsForValue();
// 声明一个带有过期时间的 string类型的数据
//TimeUnit 可以指定为小时/分钟/秒/毫秒/微秒
// stringObjectValueOperations.set("name","张三",30, TimeUnit.MINUTES);
// 对标redis中的nx操作
/* Boolean aBoolean = stringObjectValueOperations.setIfAbsent("name", "aaa");
System.out.println("aBoolean:"+aBoolean);*/
// 带有NX功能的添加
/*Boolean aBoolean = stringObjectValueOperations.setIfAbsent("name", "张三", 30, TimeUnit.MINUTES);
System.out.println("aBoolean:"+aBoolean);*/
Object value = stringObjectValueOperations.get("name");
System.out.println("value:"+value);
// 执行String的删除方法(通用)
Boolean name = redisTemplate.delete("name");
}
@Test
public void testHash(){
// HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
// boundXXX : 确定了操作的范围, 在声明的时候,根据key的参数, 就直接决定了操作的目标对象,
// key: 是redis的值的key
// hk: 是hash类型的value的field
// hv: 是hash中field对应的value值
BoundHashOperations<String, Object, Object> userOperations = redisTemplate.boundHashOps("user");
/*userOperations.put("name","林青霞");
userOperations.put("age",20);
userOperations.put("gender","女");*/
Map<String,Object> map = new HashMap<>();
map.put("name","周慧敏");
map.put("age",21);
map.put("gender","女");
userOperations.putAll(map);
userOperations.delete("age");
// 获取hash的field的方法
Object name = userOperations.get("name");
}
@After
public void after(){
// 通过解绑方法释放和redis服务的连接
// 方法中存在于一个事务 , 操作完成会自动释放(@Transactional)
RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
}
}
```
## 4.5 **opsForValue**与**boundValueOps**区别
在Spring中有redis常见的五种类型操作,分别对应的是如下:
redisTemplate.opsForValue();
redisTemplate.opsForHash();
redisTemplate.opsForList();
redisTemplate.opsForSet();
redisTemplate.opsForZSet();
redisTemplate.boundValueOps(**"key"**);
redisTemplate.boundHashOps(**"key"**);
redisTemplate.boundListOps(**"key"**);
redisTemplate.boundSetOps(**"key"**);
redisTemplate.boundZSetOps(**"key"**);
说明:opsFor的方法是拿出redis某一个类型的处理通道,而bound的方法是首先绑定某一个指定的key,再通过此key进行后续的操作。如果在实际开发场景中,某一个方法频繁对某一个key进行操作,则应该使用bound处理
## 4.6 关闭和redis的连接
springboot项目或ssm项目中, 一定要在操作redisTemplate对象的方法上,添加@Transactional注解,否则redis连接不会自动关闭,当大量请求进来的时候,redis没有及时释放前面的连接,导致连接崩溃。
**错误状态:**Could not get a resource conneciton from the pool
**错误状态:**Could not release connection to pool
如果没有添加事务注解,那么需要下面的代码进行手动释放
RedisConnectionUtils.*unbindConnection*(redisTemplate.getConnectionFactory());
# 5. 实现商品详情缓存
思路
先从缓存中查询,如果缓存中有则直接返回,如果没有则查询数据库,放入缓存中,然后再返回
数据结构的选择: string类型 , goods:1001 {desc:goodsDescString}
## 5.1 设计GoodsService
```java
public interface GoodsService { /** * 查询缓存中的商品 * @param id * @return */ public Goods queryGoodsByCacheId(Long id) throws CustomerException;}
```
## 5.2 设计GoodsServiceImpl
```java
@Service("goodsService")
@Transactional(rollbackFor = Throwable.class)
public class GoodsServiceImpl implements GoodsService {
@Resource
private RedisTemplate<String,Object> redisTemplate;
@Resource
private GoodsMapper goodsMapper;
// 指定商品key的前缀
private static final String GOODS_CACHE="goods:";
// 指定过期时间常量 (单位: 小时)
private static final Integer GOODS_CACHE_EXPIRE=2;
@Override
public Goods queryGoodsByCacheId(Long id) throws BusinessException {
// 确定操作类型为String, key的名字: GOODS_CACHE+id(goods:1)
BoundValueOperations<String, Object> goodsOps = redisTemplate.boundValueOps(GOODS_CACHE + id);
//1.先从缓存中找,如果有就直接返回
Object jsonObject = goodsOps.get();
// 获取到的应该是json格式的商品信息
//使用字符串相关的方法: isEmpty 判断字符串是否为空值/null
/* public static boolean isEmpty(@Nullable Object str) { return str == null || "".equals(str); }*/
boolean empty = StringUtils.isEmpty(jsonObject);
if(empty){
//2.如果没有就从数据库中查找,放入缓存,然后返回
//根据id查询且只查询上架的商品(status)
Goods goods = new Goods();
goods.setId(id);
goods.setStatus(1);
Goods selectedGoods = goodsMapper.selectOne(goods);
// 当商品在数据库不存在,就抛出业务异常
if(selectedGoods == null){
throw new BusinessException("没有该编号对应的商品");
}
//找到了做缓存
//将商品对象转换为json
//设置过期时间
String objectParseString = JsonUtils.objectToJson(selectedGoods);
goodsOps.set(objectParseString,GOODS_CACHE_EXPIRE, TimeUnit.HOURS);
return selectedGoods;
}else{
// 使用JsonUtils的转换方法 , 把json字符串转换为Goods对象
Goods goods = JsonUtils.jsonToObject(jsonObject.toString(), Goods.class);
return goods;
}
}
}
```
## 5.3 设计GoodsController
## 5.4 缓存双写不一致的解决方案
### 5.4.1 初级方案
问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致。
解决思路:
先删除缓存,再修改数据库,如果删除缓存成功了修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致,因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。
简言之, 当更新数据时,先删除缓存数据,然后更新数据库,等要查询的时候才把最新的数据更新到缓存
<img src="Redis%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E4%BB%8B%E7%BB%8D.assets/1363214-20190928104103320-652143063.png" alt="1363214-20190928104103320-652143063" style="zoom:80%;" />
操作分析:
- 编辑商品发生在后台,也就是admin的模块中, 当我们在admin中编辑商品信息的时候,无法调用portal中的GoodsService方法
- admin模块也不适合添加redis的操作, 这样做, 每一次admin的执行都会开启redis的连接, 失去了eshop作为聚合工程的意义
结论:
==需要在admin模块中发起想portal模块的通信,也就是"远程调用"==
方法:
使用apache的HttpClient , 并封装了工具类
```java
public class HttpClientUtil {
public static String doGet(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doGet(String url) {
return doGet(url, null);
}
public static String doPost(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (param != null) {
List<NameValuePair> paramList = new ArrayList<NameValuePair>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, param.get(key)));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return resultString;
}
public static String doPost(String url) {
return doPost(url, null);
}
public static String doPostJson(String url, String json) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return resultString;
}
}
```
使用spring自己封装的httpClient: RestTemplate
```java
public class TestHttpClient { @Test public void testPostWithoutParam() throws Exception { // 使用Spring自己的RestTemplate RestTemplate restTemplate = new RestTemplate(); URI uri = new URI("http://localhost:8081/goods/update"); String result = restTemplate.postForObject(uri, null, String.class); System.out.println(result); } @Test public void testPostWithParam() throws Exception { RestTemplate restTemplate = new RestTemplate(); URI uri = new URI("http://localhost:8080/goods/deleteCache"); // 参数 MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>(); paramMap.add("id", "1"); String result = restTemplate.postForObject(uri, paramMap, String.class); System.out.println(result); } @Test public void testPostWithForm() throws Exception { RestTemplate restTemplate = new RestTemplate(); URI uri = new URI("http://localhost:8080/user/insert"); // 参数 MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>(); paramMap.add("name","yang"); paramMap.add("phone","132"); String result = restTemplate.postForObject(uri, paramMap, String.class); System.out.println(result); } @Test public void testPostWithBody() throws Exception { RestTemplate restTemplate = new RestTemplate(); URI uri = new URI("http://localhost:8080/user/add"); //header HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); // 参数 Map<String, Object> param = new HashMap<>(); param.put("name", "yang"); param.put("phone", "136"); HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<>(param,httpHeaders); String result = restTemplate.postForObject(uri, httpEntity, String.class); System.out.println(result); }}
```
==可以将其封装为一个HttpClientUtil==
增加admin模块
添加GoodsController
注意, 此时的GoodsController是负责提供后台操作商品信息的接口, 是操作mysql的
添加GoodsService和GoodsServiceImpl
在GoodsServiceImpl中提供update方法,并通过HttpClient工具远程调用portal模块的删除缓存操作
商品修改的时候如果删除缓存调用失败,会影响修改的业务。修改商品是后台的业务,删除缓存是门户商品详情的业务,不能因为删除缓存调用失败影响修改的业务。所以我们需要把删除缓存的业务新开一个线程来执行,但是考虑性能又要使用线程池,然后还要考虑删除失败的重试机制。所以代码非常繁琐,并且很多地方都会有删除缓存的调用。所以可以把封装调用的代码单独提取抽成一个中间件。所以就出现了消息中间件这个东西。 后期我们要通过消息队列解决
### 5.4.2 并发下数据缓存不一致问题分析
问题:
第一个请求数据发生变更,先删除了缓存,然后要去修改数据库,此时还没来得及去修改;
第二个请求过来去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中;
第三个请求读取缓存中的数据 (此时第一个请求已经完成了数据库修改的操作)。
导致数据库和缓存中的数据不一样了
问题分析:
只有在对同一条数据并发读写的时候,才可能会出现这种问题。如果说并发量很低的话,特别是读并发很低,每天访问量<1万次,那么出现不一致的场景就很少 ; 但如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况
解决办法:
后期我们要通过消息队列解决
# 6. 实现秒杀业务
## 6.1 设计GoodsService
## 6.2 设计GoodsServiceImpl
```java
public JsonResult seckill(){
// 访问资源时,先通过setnx判断有没有锁(lock)
//为了让锁在锁资源过程中能够更加稳定被释放,可以利用redis中的过期时间10秒钟
// 但是, 生成key的操作(setIfAbsent)和过期时间设置的操作expire无法形成原子操作
// 解决办法: 将上述两个操作形成原子级别的操作(至少springboot2.2.4 和 tomcat9.0.24)
// 给key赋值的时候, 通过java提供UUID,把UUID作为lock的value
// 锁的失效可能性: 第一个抢到锁的用户没有完成业务且没有释放(假设15秒才完成),在该过程中,redis系统已经将锁释放,导致第二个用户拿到锁资源,
// 此时第一个用户执行了删除锁操作,使得第三个用户可以加锁...锁"消失"了
// 目标: 某个用户加锁,只能他自己解锁
// 如果还要更高的可用性, 还可以在快要到期的时候,让lock的过期时间延长(通过定时任务Timer)
// Redisson就是一个强大的锁功能
// 如果加深失败, 则秒杀失败
// 判断有没有库存(stock),如果有库存-1(用户id添加到成功秒杀用户队列)
// 如果stock>0表示有库存, 用户可以完成秒杀: 库存-1
// 为了让操作中无论业务是否出现问题,都能够正确释放锁,还需要通过异常处理保证及时释放,防止死锁发送
try{
}finally{
// 释放锁资源
// 通过判断, 在当前业务中产生的uuid作为lock的value, 和redis服务器中lock的值判断,如果相同,才表示是同一个操作的人,才能释放锁
}
}
```
## 6.2 实现GoodsController中的秒杀请求
```java
@RestController@RequestMapping("/goods")
public class GoodsController {
@Resource
private RedisTemplate<String,Object> template = null;
// jedis封装在了底层
//private StringRedisTemplate stringRedisTemplate = null;
@PetMapping("/seckill")
public JsonResult seckill(){
}
}
```
# 7. 基于jpa+redis实现单点登录sso
## 概念
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在[身份认证服务器](https://baike.baidu.com/item/身份认证服务器/10881881)上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的登录方式
## 机制
当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--ticket;用户再访问别的应用的时候就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行校验,检查ticket的合法性。如果通过校验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了
## 主要的功能
### 系统共享
统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行校验,判断其有效性。
### 信息识别
要实现SSO的功能,让用户只[登录](https://baike.baidu.com/item/登录/7182183)一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成[单点登录](https://baike.baidu.com/item/单点登录)的功能。
另外:
1、单一的用户信息数据库并不是必须的,有许多系统不能将所有的用户信息都[集中存储](https://baike.baidu.com/item/集中存储),应该允许用户信息放置在不同的存储中,事实上,只要统一认证系统,统一ticket的产生和校验,无论用户信息存储在什么地方,都能实现单点登录。
2、统一的认证系统并不是说只有单个的认证服务器
当用户在访问应用系统1时,由第一个[认证服务器](https://baike.baidu.com/item/认证服务器/17610765)进行认证后,得到由此服务器产生的ticket。当他访问应用系统2的时候,认证服务器2能够识别此ticket是由第一个服务器产生的,通过认证服务器之间标准的通讯协议来交换认证信息,仍然能够完成SSO的功能。
## 实现步骤
### 创建一个新的sso工程,和eshop平级
### 创建一个新的数据库sso
### pom文件
## SSO详细实现步骤
1. ### 概念
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在[身份认证服务器](https://baike.baidu.com/item/身份认证服务器/10881881)上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的登录方式
### 2. 机制
当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--ticket;用户再访问别的应用的时候就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行校验,检查ticket的合法性。如果通过校验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了
### 3. 主要的功能
#### 系统共享
统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行校验,判断其有效性。
#### 信息识别
要实现SSO的功能,让用户只[登录](https://baike.baidu.com/item/登录/7182183)一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成[单点登录](https://baike.baidu.com/item/单点登录)的功能。
另外:
1、单一的用户信息数据库并不是必须的,有许多系统不能将所有的用户信息都[集中存储](https://baike.baidu.com/item/集中存储),应该允许用户信息放置在不同的存储中,事实上,只要统一认证系统,统一ticket的产生和校验,无论用户信息存储在什么地方,都能实现单点登录。
2、统一的认证系统并不是说只有单个的认证服务器
当用户在访问应用系统1时,由第一个[认证服务器](https://baike.baidu.com/item/认证服务器/17610765)进行认证后,得到由此服务器产生的ticket。当他访问应用系统2的时候,认证服务器2能够识别此ticket是由第一个服务器产生的,通过认证服务器之间标准的通讯协议来交换认证信息,仍然能够完成SSO的功能。
### 4. 实现步骤
#### 4.1 创建一个新的数据库sso
jpa操作可以不添加数据表, ==在第一次操作时自动创建, ==
#### 4.2 创建一个新的sso工程,和eshop平级
##### 4.2.1 pom
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gxa</groupId>
<artifactId>sso</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--core/portal/admin...子模块都需要用到parent的基础依赖版本-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<!--自定义的组件的版本-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<swagger3.version>3.0.0</swagger3.version>
<swagger3-ui.version>1.9.6</swagger3-ui.version>
</properties>
<dependencies>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- springboot web 放在父工程中,core中操作json需要web提供的jackson依赖
BaseService单独提取出来也需要被Spring管理
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springboot整合test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--配置springboot-jdbc依赖,内置了事务管理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mysql连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--springboot2.2默认支持的mysql驱动为8.0.19-->
<!-- <version>5.1.48</version>-->
</dependency>
<!--jpa的相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--springboot整合swagger3的依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${swagger3.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>${swagger3-ui.version}</version>
</dependency>
<!--jedis的redis操作客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<!--打包时排除测试用例-->
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!--配置主启动类-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.gxa.sso.MainApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<!--可以把依赖的包都打包到生成的Jar包中-->
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<resources>
<!--通过配置依然可以识别mapper目录中的xml文件-->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<!--解决静态资源/yml配置文件等无法加载的问题-->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>
</project>
```
##### 4.2.2 编辑application-dev.yml
```yml
server:
tomcat:
# 指定字符集
uri-encoding: UTF-8
# 指定端口
port: 8080
spring:
# 配置数据源
datasource:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sso?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
jpa:
hibernate:
ddl-auto: update # 第一次创建, 之后更新
redis:
host: 139.198.183.226
port: 6379
password: '!123456Bl'
jedis:
pool:
min-idle: 1000
max-idle: -1
database: 0
```
==pa的hibernate.ddl-auto的几个属性值区别==
create:
每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
create-drop :
每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
==update==:
最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据 model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等 应用第一次运行起来后才会。
validate :
每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。
##### 4.2.3 编辑application.yml
```yml
spring:
profiles:
active: dev
```
##### 4.2.4 设计实体类 entity.User
```java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "tb_user")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(name = "user_name")
private String name;
@Column(name = "user_phone")
private String phone;
@Column(name = "user_password")
private String password;
@Column(name = "salt")
private String salt;
/**
* 登录类型: 1.手机网页端 2.pc网页端 3.手机app端
*/
@Column(name="login_type")
private Integer loginType;
/**
* 用户状态 1.正常 2.禁用
*/
@Column(name="user_status")
private Integer status;
@Column(name="last_login_time")
private Date lastLoginTime;
/**
* 手机端刷新token
*/
private String token;
}
```
##### 4.2.5 设计UserRepository
```java
//@RepositoryDefinition(domainClass = User.class,idClass = Long.class)
public interface UserRepository extends CrudRepository<User,Long> {
User findByName(String name);
List<User> findByNameStartsWith(String name);
// where phone in (?,?)
List<User> findByPhoneIn(String...phones);
// where name like '%?%' limit ?,?
Page<User> findByNameContains(String name, Pageable pageable);
//hql
//select * from tb_user where name=? and phone=?
@Query("select user from User user where name=?1 and phone =?2")
User selectByHql(String name,String phone);
@Query("select user from User user where name=:name and phone =:phone")
User selectByHql2(@Param("name") String name,@Param("phone") String phone);
@Query(nativeQuery = true,value = "select count(*)from tb_user")
Integer getCount();
}
```
##### 4.2.6 设计测试用例
```java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public class TestUserRepository {
@Resource
UserRepository userRepository;
@Test
public void testSave(){
User user = new User();
user.setName("张无忌");
user.setPhone("133");
// User save = userRepository.save(user);
// System.out.println(save);
}
@Test
public void testUpdate(){
User user = new User();
user.setId(2L);
user.setName("张无忌");
user.setPhone("131");
// User save = userRepository.save(user);
}
@Test
public void testFindByName(){
// User user = userRepository.findById(1L).get();
User user = userRepository.findByName("张无忌");
System.out.println(user);
}
@Test
public void testDeleteByName(){
User user = new User();
user.setId(1L);
// userRepository.delete(user);
}
@Test
public void testFindByNameStartingWith(){
List<User> users = userRepository.findByNameStartsWith("张");
System.out.println(users);
}
@Test
public void testFindByPhoneIn(){
List<User> byPhoneIn = userRepository.findByPhoneIn("133","131");
System.out.println(byPhoneIn);
}
@Test
public void testFindByNameContains(){
PageRequest of = PageRequest.of(0, 2);
Page<User> pageInfo = userRepository.findByNameContains("张", of);
System.out.println(pageInfo);
}
@Test
public void testHql(){
User user = userRepository.selectByHql("张三丰", "132");
System.out.println(user);
}
@Test
public void testHql2(){
User user = userRepository.selectByHql2("张三丰", "132");
System.out.println(user);
}
@Test
public void testCount(){
Integer count = userRepository.getCount();
System.out.println(count);
}
}
```
#### 4.3 注册
注册流程不变
#### 4.4 发送手机验证码
##### 4.4.1 阿里大于短信sdk的依赖
```xml
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
```
##### 4.4.2 工具类
```java
public class SendSms {
//产品名称:云通信短信API产品,开发者无需替换
static final String product = "Dysmsapi";
//产品域名,开发者无需替换
static final String domain = "dysmsapi.aliyuncs.com";
// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)
static final String accessKeyId = "your accessKeyId";
static final String accessKeySecret = "your accessKeySecret"";
// 签名
static final String sign = "your signName";
public static SendSmsResponse sendSms(String mobile, String templateCode, String templateParam) throws ClientException {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
//必填:待发送手机号
request.setPhoneNumbers(mobile);
//必填:短信签名-可在短信控制台中找到
request.setSignName(sign);
//必填:短信模板-可在短信控制台中找到
request.setTemplateCode(templateCode);
//可选:模板中的变量替换JSON串,如模板内容为"尊敬的${name},您的验证码为${code}"时,此处的值为
request.setTemplateParam(templateParam);
//hint 此处可能会抛出异常,注意catch
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
return sendSmsResponse;
}
/**
* 生成随机的6位数,短信验证码
* @return
*/
private static String getMsgCode() {
int n = 6;
StringBuilder code = new StringBuilder();
Random ran = new Random();
for (int i = 0; i < n; i++) {
code.append(Integer.valueOf(ran.nextInt(10)).toString());
}
return code.toString();
}
}
```
说明
将代码中的your accessKeyId和your accessKeySecret替换成申请或者已有的access_key和access_secret;
your phoneNumber替换成你想要接收短信的手机号码;
your signName替换申请到的签名名称;
your templateCode替换成控制台上显示的code。代码中,短信验证码code为变量,值可以自定义规则生成并替换,可以是随机生成的的6位或者其他位的数字或者字母。
##### 4.4.3 编写service
```java
public interface SmsService { public void sendLoginSms(String phone) throws CustomerException;}
```
```java
@Servicepublic class SmsServiceImpl implements SmsService {
@Resource private RedisTemplate redisTemplate;
// redis存储的key设为常量
public static final String LOGIN_SMS="login_sms:";
@Override
public void sendLoginSms(String phone) throws CustomerException {
//一键登录 有没有账号都可以发送短信
//发送登录 没有注册账号就不行
//发送注册短信 注册了就不行
//1.生成验证码(用工具类的静态方法调用)
//生成的验证码打印在控制台用于作弊获取
//2.服务器存储验证码并设置失效时间为30分钟
//3.调用工具类发送短信给用户
// String templateParam = "{\"getCode\":\""+msgCode+"\", \"storeName\":\"淘宝直营店\", \"address\":\"阿里巴巴上海分公司\",\"detail\":\"{1}号仓{2}件\"}";
SendSms.sendSms(phone,code,templateParam);
// 4. 短信接口返回的数据
//SendSmsResponse response = sendSms(mobile,templateCode,templateParam);
// System.out.println("短信接口返回的数据----------------");
//System.out.println("Code=" + response.getCode());
// OK:成功
// System.out.println("Message=" + response.getMessage());
// System.out.println("RequestId=" + response.getRequestId());
// System.out.println("BizId=" + response.getBizId());
}
}
```
##### 4.4.4 编写controller
```java
@RequestMapping("/sms")
@RestController
@Api(description = "短信相关接口文档")
public class SmsController {
@Resource
private SmsService smsService;
@RequestMapping(value = "/login")
@ApiOperation(value = "发送一键登录短信",notes = "发送一键登录短信",httpMethod = "POST")
public JsonResult sendLoginSms(@RequestParam("phone") String phone) {
if(!phone.matches("^[1](([3][0-9])|([4][5,7,9])|([5][0-9])|([6][6])|([7][3,5,6,7,8])|([8][0-9])|([9][8,9]))[0-9]{8}$")){
throw new BusinessException("手机号码格式不合法");
}
smsService.sendLoginSms(phone);
}
}
```
#### 4.5 一键登录
##### 4.5.1 编写service
```java
public interface UserService {
Map<String,String> login(String phone,String code) ;
User queryUserByToken(String token);
}
```
```java
@Service
public class UserServiceImpl implements UserService {
@Resource
private RedisTemplete redisTemplete;
@Resource
private UserRepository userRepository;
/** * 在spring和springboot中可直接对request实现注入/装配 */
@Resource
private HttpServletRequest request;
//设置token前缀
public static final String LOGIN="==key==!@#$%^";
@Override
public Map<String, String> login(String phone, String code) throws CustomerException {
//一键登录
//1.获取redis服务器存储的验证码
//验证码失效的判断,如果为空则抛出异常
// 对验证码做校验, 如果不一致则抛出异常
//校验完成, 从mysql数据库中查找用户信息
//如果用户不存在, 则匿名注册,匿名格式: "用户:"+phone , 并指定用户状态
//如果用户存在, 但状态不为1,表示冻结状态, 抛出异常
//如果用户存在,获取上次的token,如果不为空移除redis中上一次的登录信息
//生成token,存储登录信息,利用处理后的UUID
String token=UUID.randomUUID().toString().replace("-","");
//处理登录
// 设置ip,设置token,更新mysql
// redis添加token信息和过期时间
redisTemplete.set(LOGIN+token,JsonUtils.objectToJson(user),60*60*2,TimeUnit.SECEND);
Map<String,String> result=new HashMap<>();
result.put("token",token);
return result;
}
@Override
public User queryUserByToken(String token) throws BusinessException {
String result = redisTemplete.get(LOGIN + token);
if (StringUtils.isEmpty(result)) {
throw new BusinessException("无效token");
}
//要更新失效时间
redisTemplete.expire(LOGIN+token,60*60*2);
return JsonUtils.jsonToPojo(result,User.class);
}
```
e.getCode());
// OK:成功
// System.out.println("Message=" + response.getMessage());
// System.out.println("RequestId=" + response.getRequestId());
// System.out.println("BizId=" + response.getBizId());
}
}
```
##### 4.4.4 编写controller
```java
@RequestMapping("/sms")
@RestController
@Api(description = "短信相关接口文档")
public class SmsController {
@Resource
private SmsService smsService;
@RequestMapping(value = "/login")
@ApiOperation(value = "发送一键登录短信",notes = "发送一键登录短信",httpMethod = "POST")
public JsonResult sendLoginSms(@RequestParam("phone") String phone) {
if(!phone.matches("^[1](([3][0-9])|([4][5,7,9])|([5][0-9])|([6][6])|([7][3,5,6,7,8])|([8][0-9])|([9][8,9]))[0-9]{8}$")){
throw new BusinessException("手机号码格式不合法");
}
smsService.sendLoginSms(phone);
}
}
```
#### 4.5 一键登录
##### 4.5.1 编写service
```java
public interface UserService {
Map<String,String> login(String phone,String code) ;
User queryUserByToken(String token);
}
```
```java
@Service
public class UserServiceImpl implements UserService {
@Resource
private RedisTemplete redisTemplete;
@Resource
private UserRepository userRepository;
/** * 在spring和springboot中可直接对request实现注入/装配 */
@Resource
private HttpServletRequest request;
//设置token前缀
public static final String LOGIN="==key==!@#$%^";
@Override
public Map<String, String> login(String phone, String code) throws CustomerException {
//一键登录
//1.获取redis服务器存储的验证码
//验证码失效的判断,如果为空则抛出异常
// 对验证码做校验, 如果不一致则抛出异常
//校验完成, 从mysql数据库中查找用户信息
//如果用户不存在, 则匿名注册,匿名格式: "用户:"+phone , 并指定用户状态
//如果用户存在, 但状态不为1,表示冻结状态, 抛出异常
//如果用户存在,获取上次的token,如果不为空移除redis中上一次的登录信息
//生成token,存储登录信息,利用处理后的UUID
String token=UUID.randomUUID().toString().replace("-","");
//处理登录
// 设置ip,设置token,更新mysql
// redis添加token信息和过期时间
redisTemplete.set(LOGIN+token,JsonUtils.objectToJson(user),60*60*2,TimeUnit.SECEND);
Map<String,String> result=new HashMap<>();
result.put("token",token);
return result;
}
@Override
public User queryUserByToken(String token) throws BusinessException {
String result = redisTemplete.get(LOGIN + token);
if (StringUtils.isEmpty(result)) {
throw new BusinessException("无效token");
}
//要更新失效时间
redisTemplete.expire(LOGIN+token,60*60*2);
return JsonUtils.jsonToPojo(result,User.class);
}
```