# Redis6
**Repository Path**: jianghaok/redis6
## Basic Information
- **Project Name**: Redis6
- **Description**: Redis6学习笔记
- **Primary Language**: Java
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2022-04-15
- **Last Updated**: 2023-09-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
Redis6
## 一)NoSQL数据库简介
### 1.1)技术发展
技术的分类:
- 解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN
- 解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis
- 解决性能的问题:NoSQL、Java线程、Hadoop、Nginx、MQ、ElasticSearch
#### 1.1.1)Web1.0时代
Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单点服务器可以解决大部分问题。

#### 1.1.2)Web2.0时代
随着Web2.0的时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据。加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战。

##### 1.1.2.1)解决CPU及内存压力方案

Session存储问题解决方案:
1. 存储在客户端或cookie信息中
优点:每次访问请求都会带着cookie信息【里面存储着用户信息】可以保证Session共享
缺点:存储在客户端安全性无法保证
2. Session复制
将Session信息复制到每一个节点对应的服务中
缺点:Session数据冗余,节点越多浪费越大
3. 存储到NoSQL数据库
优点:无需经过I/O操作,完全在内存中,读取速度快
##### 1.1.2.2)解决IO压力方案

将经常需要查询的参数放到NoSQL缓存数据库中,减少I/O的读操作【和数据库的交互次数】
【专用的数据用特定的方式进行存储(缓存数据库/文档数据库/列式数据库)】
### 1.2)NoSQL数据库
#### 1.2.1)NoSQL数据库概述
NoSQL(**NoSQL = Not Only SQL** ),意即“不仅仅是SQL”,泛指非关系型的数据库。
NoSQL 不依赖业务逻辑方式存储【非关系型数据库】,而以简单的key-value模式存储,因此大大的增加了数据库的扩展能力。
- 不遵循SQL标准
- 不支持ACID【原子性(Atomic)一致性(Consistency)隔离性(Isolation) 持久性(Durability)】
- 远超于SQL的性能
**NoSQL适用场景**
- 对数据高并发的读写
- 海量数据的读写
- 对数据高可扩展性的
**NoSQL不适用场景**
- 需要事务支持
- 基于sql的结构化查询存储,处理复杂的关系,需要即席查询
- 【用不着sql的和用了sql也不行的情况,优先考虑NoSql】
**常见的NoSQL**
| 名称 | 特点 |
| -------- | :----------------------------------------------------------- |
| Memcache | 很早出现的NoSql数据库
数据都在内存中,一般不持久化
支持简单的key-value模式,支持类型单一
一般是作为缓存数据库辅助持久化的数据库 |
| MongoDB | 高性能、开源、模式自由(schema free)的文档型数据库
数据都在内存中, 如果内存不足,把不常用的数据保存到硬盘
虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能
支持二进制数据及大型对象
可以根据数据的特点替代RDBMS ,成为独立的数据库。或者配合RDBMS,存储特定的数据 |
| Redis | 几乎覆盖了Memcached的绝大部分功能
数据都在内存中,支持持久化,主要用作备份恢复
除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等
一般是作为缓存数据库辅助持久化的数据库 |
### 1.3)行式存储数据库(大数据时代)
#### 1.3.1)行式数据库

将每行作为一部分进行存储
优点:查询id为3的人员信息——效率高
缺点:查询年龄的平均数——效率不高
#### 1.3.2)列式数据库

将每列作为一部分进行存储
优点:查询年龄的平均数——效率高
缺点:查询id为3的人员信息——效率不高
#### 1.3.3)Hbase
HBase是Hadoop项目中的数据库,它用于需要对大量的数据进行随机、实时的读写操作的场景中。
HBase的目标就是处理数据量非常庞大的表,可以用普通的计算机处理超过10亿行数据,还可处理有数百万列元素的数据表。
### 1.4)图关系型数据库
主要应用:社会关系,公共交通网络,地图及网络拓谱(n*(n-1)/2)

## 二)Redis概述和安装
### 2.1)Redis概述
Redis是一个开源的key-value存储系统:
- Redis支持存储的value类型包括:string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型);这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的;在此基础上,Redis还支持各种不同方式的排序;
- Redis为了保证效率,数据都是缓存在内存中;但会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
#### 2.1.1)应用场景
##### 2.1.1.1)配合关系型数据库做高速缓存
- 高频次,热门访问的数据,降低数据库IO
- 分布式架构,做session共享
##### 2.1.1.2)多样的数据结构存储持久化数据

### 2.2)Redis安装
Redis官网:http://redis.io
Redis中文官网:http://redis.cn/
如下图,进行下载:

上传至Linux系统中
#### 2.2.1)安装步骤
准备工作:下载安装最新版的gcc编译器
进入Linux系统的Root用户,安装gcc 编译环境
```
yum install gcc
```
安装完成后输入下列命令进行检验
```
gcc --version
```
解压 redis压缩包
```
tar -zxvf redis-6.2.6.tar.gz
```
进入解压后的 redis-6.2.6文件,输入 make 命令进行编译
当看到 'make test' 后,再执行命令:
```
make install
```
成功安装后,在 /usr/local/bin 目录下可以看到 Redis 的相关服务

相应模块用途如下:
- redis-benchmark:性能测试工具,可以在自己环境运行,看看自己服务器性能如何
- redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲
- redis-check-dump:修复有问题的dump.rdb文件
- redis-sentinel:Redis集群使用
- redis-server:Redis服务器启动命令
- redis-cli:客户端,操作入口
#### 2.2.2)前台启动(不推荐)
前台启动,命令行窗口不能关闭,否则服务器停止,命令启动
```
redis-server
```

#### 2.2.3)后台启动(推荐)
修改redis目录下的配置文件 redis.conf,将daemonize no改成yes,如下图:

进入 /usr/local/bin 目录下,执行命令即可启动 redis 服务
```
redis-server /root/redis/redis-6.2.6/redis.conf
```
启动及验证如下图:

#### 2.2.4)Redis关闭
单实例关闭:
```
redis-cli shutdown
```
也可以进入终端后再关闭,如下图:

也可以直接根据进程号,kill -9 + pid号
#### 2.2.5)Redis介绍相关知识
- 默认16个数据库,类似数组下标从0开始,初始默认使用0号库
- 使用命令 select 来切换数据库。如: select 8
- 统一密码管理,所有库同样密码
- dbsize查看当前数据库的key的数量
- flushdb清空当前库
- flushall通杀全部库
##### 2.2.5.1)多路复用
指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)
**串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)**
(与Memcache三点不同: Redis 支持多数据类型,支持持久化,单线程+多路IO复用)
单线程+多路IO复用(Redis)说明:

买票的人在不确定黄牛是否可以买到票时,无需等待,继续做其他事,直到黄牛买到票后返回给他,体现了多路IO复用的思路,而黄牛与火车站是一对一的关系,体现了单线程
## 三)Redis常用五大数据类型
redis常见数据类型操作命令: http://www.redis.cn/commands.html
### 3.1)Redis键(key)【不属于数据类型】
登录Linux系统中 /usr/local/bin 目录下的 redis-cli 客户端
set key value : 往Redis中插入键(key)形式的数据
```
127.0.0.1:6379> set k1 luck
OK
127.0.0.1:6379> set k2 luck2
OK
127.0.0.1:6379> set k3 luck3
OK
```
keys *:查看当前库所有key (匹配:keys *1)
```
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
```
exists key:判断某个key是否存在
```
127.0.0.1:6379> exists k1
(integer) 1
127.0.0.1:6379> exists k4
(integer) 0
```
type key :查看你的key是什么类型
```
127.0.0.1:6379> type k2
string
```
del key:删除指定的key数据
```
127.0.0.1:6379> del k3
(integer) 1
127.0.0.1:6379> keys *
1) "k2"
```
unlink key :根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作【演示效果相同,但会之后在内部删除,并不是当时就删了】
```
127.0.0.1:6379> unlink k2
(integer) 1
127.0.0.1:6379> keys *
1) "k1"
```
expire key 10:10秒钟:为给定的key设置过期时间
```
127.0.0.1:6379> expire k1 10
(integer) 1
```
ttl key:查看还有多少秒过期,-1表示永不过期,-2表示已过期 ,已过期的数据就查询不到了
```
127.0.0.1:6379> ttl k1
(integer) 4
127.0.0.1:6379> ttl k1
(integer) -2
127.0.0.1:6379> keys *
(empty array)
```
select:命令切换数据库 ,默认是 0 号库
```
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> select 10
OK
127.0.0.1:6379[10]> select 0
OK
```
dbsize:查看当前数据库的key的数量
```
127.0.0.1:6379> set k1 luck
OK
127.0.0.1:6379> dbsize
(integer) 1
```
flushdb:清空当前库
```
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)
```
flushall:通杀全部库
```
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty array)
```
### 3.2)Redis字符串(String)
#### 3.2.1)简介
String是Redis最基本的类型,可以理解成与Memcached一模一样的类型,一个key对应一个value;
String类型是二进制安全的;意味着Redis的string可以包含任何数据,比如jpg图片或者序列化的对象;
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
#### 3.2.2)常用命令
set :添加键值对
*NX:当数据库中key不存在时,可以将key-value添加数据库
*XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
*EX:key的超时秒数
*PX:key的超时毫秒数,与EX互斥
```
127.0.0.1:6379> set k1 s1
OK
127.0.0.1:6379> set k2 s2
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
```
get :查询对应键值【key值相同的再次赋值会覆盖原值】
```
127.0.0.1:6379> get k1
"s1"
127.0.0.1:6379> set k1 s11
OK
127.0.0.1:6379> get k1
"s11"
```
append 将给定的 追加到原值的末尾
```
127.0.0.1:6379> append k1 +1
(integer) 5
127.0.0.1:6379> get k1
"s11+1"
```
strlen :获得值的长度
```
127.0.0.1:6379> strlen k1
(integer) 5
```
setnx :只有在 key 不存在时 设置 key 的值【key值不存在时才可以赋值成功】
```
127.0.0.1:6379> setnx k1 s12
(integer) 0
127.0.0.1:6379> get k1
"s11+1"
127.0.0.1:6379> setnx k3 s3
(integer) 1
127.0.0.1:6379> get k3
"s3"
```
incr :将 key 中储存的数字值增1【只能对数字值操作,如果为空,新增值为1】
```
127.0.0.1:6379> set k4 4
OK
127.0.0.1:6379> get k4
"4"
127.0.0.1:6379> incr k4
(integer) 5
127.0.0.1:6379> get k4
"5"
127.0.0.1:6379> incr k5
(integer) 1
127.0.0.1:6379> get k5
"1"
```
decr :将 key 中储存的数字值减1,只能对数字值操作,如果为空,新增值为-1
```
127.0.0.1:6379> decr k4
(integer) 4
127.0.0.1:6379> get k4
"4"
127.0.0.1:6379> decr k6
(integer) -1
127.0.0.1:6379> get k6
"-1"
```
incrby / decrby <步长> :将 key 中储存的数字值增减。自定义步长
```
127.0.0.1:6379> get k4
"4"
127.0.0.1:6379> incrby k4 2
(integer) 6
127.0.0.1:6379> get k4
"6"
127.0.0.1:6379> decrby k4 3
(integer) 3
127.0.0.1:6379> get k4
"3"
```
mset ..... :同时设置一个或多个 key-value对
```
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> mset k1 s1 k2 s2 k3 s3
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
```
mget ..... :同时获取一个或多个 value
```
127.0.0.1:6379> mget k1 k2 k3
1) "s1"
2) "s2"
3) "s3"
```
msetnx ..... :同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在【原子性,有一个失败则都失败】
```
127.0.0.1:6379> msetnx k11 s11 k12 s12 k3 s3
(integer) 0
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> msetnx k11 s11 k12 s12 k13 s13
(integer) 1
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k13"
4) "k3"
5) "k12"
6) "k11"
```
getrange <起始位置><结束位置> :获得值的范围,类似java中的substring,前包,后包
```
127.0.0.1:6379> set name luck
OK
127.0.0.1:6379> getrange name 1 3
"uck"
```
setrange <起始位置> :用 覆写所储存的字符串值,从<起始位置>开始(索引从0开始);
```
127.0.0.1:6379> setrange name 3 add
(integer) 6
127.0.0.1:6379> get name
"lucadd"
```
setex <过期时间> :设置键值的同时,设置过期时间,单位秒;
```
127.0.0.1:6379> setex age 10 age1
OK
127.0.0.1:6379> ttl age
(integer) 4
127.0.0.1:6379> ttl age
(integer) -2
127.0.0.1:6379> get age
(nil)
```
getset :以新换旧,设置了新值同时获得旧值。
```
127.0.0.1:6379> get name
"lucadd"
127.0.0.1:6379> getset name luck
"lucadd"
127.0.0.1:6379> get name
"luck"
```
#### 3.2.3)原子性
incr key —— 对存储在指定 key 的数值执行原子的 +1 操作
原子性:指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)
(1)在单线程中, 能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间;
(2)在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
Redis单命令的原子性主要得益于Redis的单线程。
案例如下图:

#### 3.2.4)String的数据结构
String的数据结构为**简单动态字符串**(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
### 3.3)Redis列表(List)
#### 3.3.1)简介
Redis 列表【单键多值】是简单的字符串列表,按照插入顺序排序;
可以添加一个元素到列表的头部(左边)或者尾部(右边)''它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

#### 3.3.2)常用命令
lpush/rpush .... :从左边/右边插入一个或多个值
```
127.0.0.1:6379> lpush k1 v1 v2 v3
(integer) 3
127.0.0.1:6379> lrange k1 0 -1
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> rpush k2 v1 v2 v3
(integer) 3
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v2"
3) "v3"
```
数据插入顺序如下:
| k1【左插入】 | v3 | v2 | v1 |
| ------------ | ---- | ---- | ---- |
| k2【右插入】 | v1 | v2 | v3 |
lpop/rpop :从左边/右边吐出一个值。值在键在,值光键亡
```shell
127.0.0.1:6379> lpop k1
"v3"
127.0.0.1:6379> rpop k2
"v3"
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
127.0.0.1:6379> rpop k2
"v2"
127.0.0.1:6379> rpop k2
"v1"
127.0.0.1:6379> keys *
1) "k1"
```
rpoplpush :从列表右边吐出一个值,插到列表左边
```sh
127.0.0.1:6379> keys *
1) "k1"
127.0.0.1:6379> rpush k2 v11 v12 v13
(integer) 3
127.0.0.1:6379> lrange k1 0 -1
1) "v2"
2) "v1"
127.0.0.1:6379> lrange k2 0 -1
1) "v11"
2) "v12"
3) "v13"
127.0.0.1:6379> rpoplpush k1 k2
"v1"
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v11"
3) "v12"
4) "v13"
```
lrange :按照索引下标获得元素(从左到右),lrange mylist 0 -1 0左边第一个,-1右边第一个,(0-1表示获取所有)
```shell
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v11"
3) "v12"
4) "v13"
```
lindex :按照索引下标获得元素(从左到右)
```shell
127.0.0.1:6379> lindex k2 0
"v1"
127.0.0.1:6379> lindex k2 2
"v12"
```
llen :获得列表长度
```shell
127.0.0.1:6379> llen k2
(integer) 4
```
linsert before :在的后面插入插入值
```shell
127.0.0.1:6379> linsert k2 before "v11" "newv11"
(integer) 5
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "newv11"
3) "v11"
4) "v12"
5) "v13"
127.0.0.1:6379> linsert k2 after "v11" "afterv11"
(integer) 6
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "newv11"
3) "v11"
4) "afterv11"
5) "v12"
6) "v13"
```
lrem :从左边删除n个value(从左到右)
```shell
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "newv11"
3) "v11"
4) "afterv11"
5) "v12"
6) "v13"
127.0.0.1:6379> lrem k2 2 "newv11"
(integer) 1
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v11"
3) "afterv11"
4) "v12"
5) "v13"
```
lset :将列表key下标为index的值替换成value
```shell
127.0.0.1:6379> lset k2 2 "newv11"
OK
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v11"
3) "newv11"
4) "v12"
5) "v13"
```
#### 3.3.3)List的数据结构
List的数据结构为快速链表quickList。
- 在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表;它将所有的元素紧挨着一起存储,分配的是一块连续的内存;
- 当数据量比较多的时候才会改成quicklist;因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

Redis将链表和ziplist结合起来组成了quicklist;也就是将多个ziplist使用双向指针串起来使用,这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
### 3.4)Redis集合(Set)
#### 3.4.1)简介
Redis set 对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)【一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变】
#### 3.4.2)常用命令
sadd ..... :将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
```
127.0.0.1:6379> sadd k1 v1 v2 v3
(integer) 3
```
smembers :取出该集合的所有值
```
127.0.0.1:6379> smembers k1
1) "v1"
2) "v3"
3) "v2"
```
sismember :判断集合是否为含有该值,有1,没有0
```
127.0.0.1:6379> sismember k1 v1
(integer) 1
127.0.0.1:6379> sismember k1 v4
(integer) 0
```
scard:返回该集合的元素个数
```
127.0.0.1:6379> scard k1
(integer) 3
```
srem .... :删除集合中的某个元素
```
127.0.0.1:6379> srem k1 v1
(integer) 1
127.0.0.1:6379> smembers k1
1) "v3"
2) "v2"
```
spop :随机从该集合中吐出一个值,会将该值删除,当 Set 中没有值时,会删除该 Set
```
127.0.0.1:6379> sadd k2 v1 v2 v3
(integer) 3
127.0.0.1:6379> smembers k2
1) "v1"
2) "v3"
3) "v2"
127.0.0.1:6379> spop k2
"v1"
127.0.0.1:6379> spop k2
"v3"
127.0.0.1:6379> spop k2
"v2"
127.0.0.1:6379> keys *
1) "k1"
```
srandmember :随机从该集合中取出n个值。不会从集合中删除
```
127.0.0.1:6379> srandmember k1
"v3"
127.0.0.1:6379> srandmember k1
"v2"
127.0.0.1:6379> smembers k1
1) "v3"
2) "v2"
```
smove value:把集合中一个值从一个集合移动到另一个集合
```
127.0.0.1:6379> sadd k1 v1 v2 v3
(integer) 3
127.0.0.1:6379> sadd k2 v11 v12 v13
(integer) 3
127.0.0.1:6379> smove k1 k2 v2
(integer) 1
127.0.0.1:6379> smembers k1
1) "v1"
2) "v3"
127.0.0.1:6379> smembers k2
1) "v12"
2) "v2"
3) "v13"
4) "v11"
```
sinter :返回两个集合的交集元素
```
127.0.0.1:6379> smembers k1
1) "v1"
2) "v3"
127.0.0.1:6379> sadd k3 v3 v4 v5
(integer) 3
127.0.0.1:6379> sinter k1 k3
1) "v3"
```
sunion :返回两个集合的并集元素
```
127.0.0.1:6379> sunion k1 k3
1) "v1"
2) "v4"
3) "v5"
4) "v3"
```
sdiff :返回两个集合的差集元素(key1中的,不包含key2中的)
```
127.0.0.1:6379> sdiff k1 k3
1) "v1"
```
#### 3.4.3)Set的数据结构
Set数据结构是dict字典,字典是用哈希表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。
Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
### 3.5)Redis哈希(Hash)
#### 3.5.1)简介
Redis hash 是一个键值对集合:
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的Map;用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:
方式一:将value中要存储的对象进行序列化,如转换成 JSON字符串的形式进行存储

缺点:每一次需要修改对象中的内容时,都要先将字符串转换为对象,修改完对象的值后再转换成JSON字符串进行存储,【先反序列化改好后再序列化回去】开销较大
方式二:将value中要存储的对象内容分开存储

优点:修改对象的内容可以直接进行修改,无需其他操作
缺点:每一个对象的值存储分散,当存在多个对象需要存储时,会造成数据冗余且数据混乱,难以管理
上述两种用普通的key/value结构来存储的方式均不推荐,推荐使用下面的哈希(Hash)存储
方式三:使用哈希(Hash)存储

通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了
优点:既不需要重复存储数据,也不会带来序列化和并发修改控制的问题
#### 3.5.2)常用命令
hset :给集合中的 键赋值
```shell
127.0.0.1:6379> hset user:1001 id 1
(integer) 1
127.0.0.1:6379> hset user:1001 name zhangsan
(integer) 1
```
hget : 从集合取出 value
```shell
127.0.0.1:6379> hget user:1001 id
"1"
127.0.0.1:6379> hget user:1001 name
"zhangsan"
```
hmset ... :批量设置hash的值
```shell
127.0.0.1:6379> hmset user:1002 id 2 name lisi age 30
OK
```
hexists :查看哈希表 key 中,给定域 field 是否存在
```shell
127.0.0.1:6379> hexists user:1002 id
(integer) 1
127.0.0.1:6379> hexists user:1002 gender
(integer) 0
```
hkeys :列出该hash集合的所有field
```shell
127.0.0.1:6379> hkeys user:1002
1) "id"
2) "name"
3) "age"
```
hvals :列出该hash集合的所有value
```shell
127.0.0.1:6379> hvals user:1002
1) "2"
2) "lisi"
3) "30"
```
hincrby :为哈希表 key 中的域 field 的值加上增量 1 -1
```shell
127.0.0.1:6379> hincrby user:1002 age 2
(integer) 32
```
hsetnx :将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在
```shell
127.0.0.1:6379> hsetnx user:1002 age 2
(integer) 0
127.0.0.1:6379> hsetnx user:1002 gender 1
(integer) 1
127.0.0.1:6379> hvals user:1002
1) "2"
2) "lisi"
3) "32"
4) "1"
```
#### 3.5.3)Hash的数据结构
Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。
当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
### 3.6)Redis有序集合Zset(sorted set)
#### 3.6.1) 简介
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
因为元素是有序的, 所以可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此能够使用有序集合作为**一个没有重复成员的智能列表**。
#### 3.6.2)常用命令
zadd … :将一个或多个 member 元素及其 score 值加入到有序集 key 当中
```
127.0.0.1:6379> zadd topn 200 java 300 c++ 400 mysql 500 php
(integer) 4
```
zrange [WITHSCORES] :返回有序集 key 中,下标在之间的元素
带WITHSCORES,可以让分数一起和值返回到结果集【从小到大排序】
```
127.0.0.1:6379> zrange topn 0 -1
1) "java"
2) "c++"
3) "mysql"
4) "php"
```
zrangebyscore key minmax [withscores] [limit offset count] :返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列
```
127.0.0.1:6379> zrangebyscore topn 300 500
1) "c++"
2) "mysql"
3) "php"
127.0.0.1:6379> zrangebyscore topn 300 500 withscores
1) "c++"
2) "300"
3) "mysql"
4) "400"
5) "php"
6) "500"
```
zrevrangebyscore key maxmin [withscores] [limit offset count] 同上,改为从大到小排列
```
127.0.0.1:6379> zrevrangebyscore topn 500 200
1) "php"
2) "mysql"
3) "c++"
4) "java"
127.0.0.1:6379> zrevrangebyscore topn 500 200 withscores
1) "php"
2) "500"
3) "mysql"
4) "400"
5) "c++"
6) "300"
7) "java"
8) "200"
```
zincrby :为元素的score加上增量
```
127.0.0.1:6379> zincrby topn 50 java
"250"
```
zrem :删除该集合下,指定值的元素
```
127.0.0.1:6379> zrange topn 0 -1
1) "java"
2) "c++"
3) "mysql"
4) "php"
127.0.0.1:6379> zrem topn php
(integer) 1
127.0.0.1:6379> zrange topn 0 -1
1) "java"
2) "c++"
3) "mysql"
```
zcount :统计该集合,分数区间内的元素个数
```
127.0.0.1:6379> zcount topn 200 300
(integer) 2
```
zrank :返回该值在集合中的排名,从0开始
```
127.0.0.1:6379> zrange topn 0 -1
1) "java"
2) "c++"
3) "mysql"
127.0.0.1:6379> zrank topn java
(integer) 0
127.0.0.1:6379> zrank topn mysql
(integer) 2
```
#### 3.6.3)Zset的数据结构
SortedSet(Zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
##### 3.6.3.1)跳跃表(跳表)简介
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。
对于有序集合的底层实现,可以用数组、平衡树、链表等:
- 数组不便元素的插入、删除;
- 平衡树或红黑树虽然效率高但结构复杂;
- 链表查询需要遍历所有效率低;
Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。
##### 3.6.3.2)跳跃表(跳表)实例
对比有序链表和跳跃表,从链表中查询出51
1)有序链表

要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较
2)跳跃表

从第2层开始,1节点比51节点小,向后比较;
21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层;
在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下;
在第0层,51节点为要查找的节点,节点被找到,共查找4次。
从此可以看出跳跃表比有序链表效率要高
## 四)Redis配置文件详解
### 4.1)Units单位
配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit,对大小写不敏感
```
# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf
# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
```
### 4.2)INCLUDES包含
类似jsp中的include,多实例的情况可以把公用的配置文件提取出来
```
################################## INCLUDES ###################################
# Include one or more other config files here. This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings. Include files can include
# other files, so use this wisely.
#
# Note that option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf
```
### 4.3)网络相关配置
#### 4.3.1)bind
默认情况bind=127.0.0.1,只能接受本机的访问请求,不写的情况下,无限制接受任何ip地址的访问,为了能够让除本机的其余服务器也能远程访问,将 bind 127.0.0.1 -::1注释掉
```
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT OUT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#bind 127.0.0.1 -::1
```
#### 4.3.2)protected-mode
为了能够让除本机的其余服务器也能远程访问,将 protected-mode yes 修改为 protected-mode no
```
# Protected mode is a layer of security protection, in order to avoid that
# Redis instances left open on the internet are accessed and exploited.
#
# When protected mode is on and if:
#
# 1) The server is not binding explicitly to a set of addresses using the
# "bind" directive.
# 2) No password is configured.
#
# The server only accepts connections from clients connecting from the
# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain
# sockets.
#
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode no
```
#### 4.3.3)Port
端口号,默认 6379
```
# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
port 6379
```
#### 4.3.4)tcp-backlog
设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列。在高并发环境下需要一个高backlog值来避免慢客户端连接问题。
注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值(128),所以需要确认增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)两个值来达到想要的效果
```
# TCP listen() backlog.
#
# In high requests-per-second environments you need a high backlog in order
# to avoid slow clients connection issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
# in order to get the desired effect.
tcp-backlog 511
```
#### 4.3.5)timeout
一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。
```
# Unix socket.
#
# Specify the path for the Unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
# unixsocket /run/redis.sock
# unixsocketperm 700
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
```
#### 4.3.6)tcp-keepalive
对访问客户端的一种心跳检测,每个n秒检测一次。如果存活则继续执行服务,不过不存活则断开连接;
单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
```
# TCP keepalive.
#
# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
# of communication. This is useful for two reasons:
#
# 1) Detect dead peers.
# 2) Force network equipment in the middle to consider the connection to be
# alive.
#
# On Linux, the specified value (in seconds) is the period used to send ACKs.
# Note that to close the connection the double of the time is needed.
# On other kernels the period depends on the kernel configuration.
#
# A reasonable value for this option is 300 seconds, which is the new
# Redis default starting with Redis 3.2.1.
tcp-keepalive 300
```
### 4.4)GENERAL通用
#### 4.4.1)daemonize
是否为后台进程,设置为yes,守护进程,后台启动
```
################################# GENERAL #####################################
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
# When Redis is supervised by upstart or systemd, this parameter has no impact.
daemonize yes
```
#### 4.4.2)pidfile
存放pid文件的位置,每个实例会产生一个不同的pid文件
```
# If a pid file is specified, Redis writes it where specified at startup
# and removes it at exit.
#
# When the server runs non daemonized, no pid file is created if none is
# specified in the configuration. When the server is daemonized, the pid file
# is used even if not specified, defaulting to "/var/run/redis.pid".
#
# Creating a pid file is best effort: if Redis is not able to create it
# nothing bad happens, the server will start and run normally.
#
# Note that on modern Linux systems "/run/redis.pid" is more conforming
# and should be used instead.
pidfile /var/run/redis_6379.pid
```
#### 4.4.3)loglevel
指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice;
四个级别根据使用阶段来选择,生产环境选择notice 或者warning
```
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice
```
#### 4.4.4)logfile
日志文件名称
```
# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile ""
```
4.4.5)databases 16
设定库的数量 默认16,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
```
# To disable the fast memory check that's run as part of the crash log, which
# will possibly let redis terminate sooner, uncomment the following:
#
# crash-memcheck-enabled no
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT where
# dbid is a number between 0 and 'databases'-1
databases 16
```
### 4.5)SECURITY安全
#### 4.5.1)设置密码
访问密码的查看、设置和取消,将 # requirepass foobared 的注释取消
```
# IMPORTANT NOTE: starting with Redis 6 "requirepass" is just a compatibility
# layer on top of the new ACL system. The option effect will be just setting
# the password for the default user. Clients will still authenticate using
# AUTH as usually, or more explicitly with AUTH default
# if they follow the new protocol: both will work.
#
# The requirepass is not compatable with aclfile option and the ACL LOAD
# command, these will cause requirepass to be ignored.
#
# requirepass foobared
```
在命令中设置密码,只是临时的。重启redis服务器,密码就还原了。
永久设置,需要再配置文件中进行设置。

### 4.6)LIMITS限制
#### 4.6.1)maxclients
- 设置redis同时可以与多少个客户端进行连接;
- 默认情况下为10000个客户端;
- 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应
```
# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# IMPORTANT: When Redis Cluster is used, the max number of connections is also
# shared with the cluster bus: every node in the cluster will use two
# connections, one incoming and another outgoing. It is important to size the
# limit accordingly in case of very large clusters.
#
# maxclients 10000
```
#### 4.6.2)maxmemory
建议必须设置,否则,将内存占满,造成服务器宕机
- 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。
- 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
- 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。
```
############################## MEMORY MANAGEMENT ################################
# Set a memory usage limit to the specified amount of bytes.
# When the memory limit is reached Redis will try to remove keys
# according to the eviction policy selected (see maxmemory-policy).
#
# If Redis can't remove keys according to the policy, or if the policy is
# set to 'noeviction', Redis will start to reply with errors to commands
# that would use more memory, like SET, LPUSH, and so on, and will continue
# to reply to read-only commands like GET.
#
# This option is usually useful when using Redis as an LRU or LFU cache, or to
# set a hard memory limit for an instance (using the 'noeviction' policy).
#
# WARNING: If you have replicas attached to an instance with maxmemory on,
# the size of the output buffers needed to feed the replicas are subtracted
# from the used memory count, so that network problems / resyncs will
# not trigger a loop where keys are evicted, and in turn the output
# buffer of replicas is full with DELs of keys evicted triggering the deletion
# of more keys, and so forth until the database is completely emptied.
#
# In short... if you have replicas attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for replica
# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory
```
#### 4.6.3)maxmemory-policy
volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
allkeys-lru:在所有集合key中,使用LRU算法移除key
volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
allkeys-random:在所有集合key中,移除随机的key
volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
noeviction:不进行移除。针对写操作,只是返回错误信息
```
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select one from the following behaviors:
#
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
```
#### 4.6.3)maxmemory-samples
- 设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个;
- 一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。
```
# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. By default Redis will check five keys and pick the one that was
# used least recently, you can change the sample size using the following
# configuration directive.
#
# The default of 5 produces good enough results. 10 Approximates very closely
# true LRU but costs more CPU. 3 is faster but not very accurate.
#
# maxmemory-samples 5
```
## 五)Redis的发布和订阅
### 5.1)什么是发布和订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
### 5.2)Redis的发布和订阅
1、客户端可以订阅频道如下图

2、当给这个频道发布消息后,消息就会发送给订阅的客户端

### 5.3)发布订阅命令行实现
1、 打开一个客户端订阅channel1
```
[root@VM-20-6-centos bin]# redis-cli
127.0.0.1:6379> SUBSCRIBE channel1
```
2、打开另一个客户端,给channel1发布消息hello
```
[root@VM-20-6-centos bin]# redis-cli
127.0.0.1:6379> publish channel1 hello
```
3、打开第一个客户端可以看到发送的消息
```
127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hello"
```
【发布的消息没有持久化,如果在订阅的客户端收不到hello,只能收到订阅后发布的消息】
## 六)Redis新数据类型
### 6.1)Bitmaps
#### 6.1.1)简介
现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图:

合理地使用操作位能够有效地提高内存使用率和开发效率。
Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
- Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value),但是它可以对字符串的位进行操作。
- Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

#### 6.1.2)命令
##### 6.1.2.1)setbit
1.1)格式
setbit:设置Bitmaps中某个偏移量的值(0或1) 【*offset:偏移量从0开始】
1.2)实例
每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图:

unique:users:20210101代表2020-01-01这天的独立访问用户的Bitmaps
```
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> setbit users:20210101 1 1
(integer) 0
127.0.0.1:6379> setbit users:20210101 6 1
(integer) 0
127.0.0.1:6379> setbit users:20210101 11 1
(integer) 0
127.0.0.1:6379> setbit users:20210101 15 1
(integer) 0
127.0.0.1:6379> setbit users:20210101 19 1
(integer) 0
```
很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。
在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞。
##### 6.1.2.2)getbit
2.1)格式
getbit :获取Bitmaps中某个偏移量的值
获取键的第offset位的值(从0开始算)
2.2)实例
获取id=8的用户是否在2021-01-01这天访问过, 返回0说明没有访问过:【不存在的也返回0】
```
127.0.0.1:6379> getbit users:20210101 1
(integer) 1
127.0.0.1:6379> getbit users:20210101 6
(integer) 1
127.0.0.1:6379> getbit users:20210101 8
(integer) 0
```
##### 6.1.2.3)bitcount
统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含。
3.1)格式
bitcount[start end] :统计字符串从start字节到end字节比特值为1的数量
3.2)实例
计算2021-01-01这天的独立访问用户数量,start和end代表起始和结束字节数, 下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数, 对应的用户id是11, 15, 19。
```
127.0.0.1:6379> bitcount users:20210101
(integer) 5
127.0.0.1:6379> bitcount users:20210101 0 -1
(integer) 5
127.0.0.1:6379> bitcount users:20210101 0 10
(integer) 5
127.0.0.1:6379> bitcount users:20210101 1 2
(integer) 3
```
举例: K1 【01000001 01000000 00000000 00100001】,对应【0,1,2,3】
bitcount K1 1 2 : 统计下标1、2字节组中bit=1的个数,即01000000 00000000
--》bitcount K1 1 2 --》1
bitcount K1 1 3 : 统计下标1、2字节组中bit=1的个数,即01000000 00000000 00100001
--》bitcount K1 1 3 --》3
bitcount K1 0 -2 : 统计下标0到下标倒数第2,字节组中bit=1的个数,即01000001 01000000 00000000
--》bitcount K1 0 -2 --》3
注意:redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置。
##### 6.1.2.4)bitop
4.1)格式
bitop and(or/not/xor) [key…]
bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
4.2)实例
2022-04-16 日访问网站的userid=1,2,5,9
```
127.0.0.1:6379> setbit unique:users:20220416 1 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20220416 2 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20220416 5 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20220416 9 1
(integer) 0
```
2022-04-15 日访问网站的userid=0,1,4,9。
```
127.0.0.1:6379> setbit unique:users:20220415 0 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20220415 1 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20220415 4 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20220415 9 1
```
计算出两天都访问过网站的用户数量
```
127.0.0.1:6379> bitop and unique:users:and:20220416_15 unique:users:20220415 unique:users:20220416
(integer) 2
```
过程如下:

#### 6.1.3)Bitmaps与set对比
假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表:
| set和Bitmaps存储一天活跃用户对比 | | | |
| -------------------------------- | ------------------ | ---------------- | ---------------------- |
| 数据类型 | 每个用户id占用空间 | 需要存储的用户量 | 全部内存量 |
| 集合类型 | 64位 | 50000000 | 64位*50000000 = 400MB |
| Bitmaps | 1位 | 100000000 | 1位*100000000 = 12.5MB |
很明显,这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的
| set和Bitmaps存储独立用户空间对比 | | | |
| -------------------------------- | ------ | ------ | ----- |
| 数据类型 | 一天 | 一个月 | 一年 |
| 集合类型 | 400MB | 12GB | 144GB |
| Bitmaps | 12.5MB | 375MB | 4.5GB |
但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。
| set和Bitmaps存储一天活跃用户对比(独立用户比较少) | | | |
| -------------------------------------------------- | ------------------ | ---------------- | ---------------------- |
| 数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部内存量 |
| 集合类型 | 64位 | 100000 | 64位*100000 = 800KB |
| Bitmaps | 1位 | 100000000 | 1位*100000000 = 12.5MB |
### 6.2)HyperLogLog
#### 6.2.1)简介
在工作当中,经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?
这种求集合中不重复元素个数的问题称为基数问题, 解决基数问题有很多种方案:
- 数据存储在MySQL表中,使用distinct count计算不重复个数
- 使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
能否能够降低一定的精度来平衡存储空间?
Redis推出了HyperLogLog,Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,**在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的**。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
#### 6.2.2)命令
##### 6.2.2.1)pfadd
1)格式
pfadd < element> [element ...] :添加指定元素到 HyperLogLog 中
2)实例
将所有元素添加到指定HyperLogLog数据结构中,如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0
```
127.0.0.1:6379> pfadd program "java"
(integer) 1
127.0.0.1:6379> pfadd program "php"
(integer) 1
127.0.0.1:6379> pfadd program "java" "c++"
(integer) 1
127.0.0.1:6379> pfadd program "java"
(integer) 0
```
###### 6.2.2.2)pfcount
1)格式
pfcount [key ...] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
2)实例
```
127.0.0.1:6379> pfcount program
(integer) 3
```
###### 6.2.2.3)pfmerge
1)格式
pfmerge [sourcekey ...] :将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
2)实例
新建 k1,合并k1和program为k2
```
127.0.0.1:6379> pfadd k1 "a"
(integer) 1
127.0.0.1:6379> pfadd k1 "b"
(integer) 1
127.0.0.1:6379> pfcount k1
(integer) 2
127.0.0.1:6379> pfcount program
(integer) 3
127.0.0.1:6379> pfmerge k2 k1 program
OK
127.0.0.1:6379> pfcount k2
(integer) 5
```
### 6.3)Geospatial
#### 6.3.1)简介
Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。
redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
#### 6.3.1)命令
##### 6.3.1.1)geoadd
1)格式
geoadd< longitude> [longitude latitude member...] :添加地理位置(经度,纬度,名称)
2)实例
```
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen 116.38 39.90 beijing
(integer) 3
```
##### 6.3.1.2)geopos
1)格式
geopos [member...] : 获得指定地区的坐标值
2)实例
```
127.0.0.1:6379> geopos china:city shanghai
1) 1) "121.47000163793563843"
2) "31.22999903975783553"
127.0.0.1:6379> geopos china:city beijing
1) 1) "116.38000041246414185"
2) "39.90000009167092543"
```
##### 6.3.1.3)geodist
1)格式
geodist [m|km|ft|mi ] :获取两个位置之间的直线距离
2)实例
获取两个位置之间的直线距离
```
127.0.0.1:6379> geodist china:city beijing shanghai km
"1068.1535"
```
##### 6.3.1.4)georadius
1)格式
georadius< longitude>radius m|km|ft|mi : 以给定的经纬度为中心,找出某一半径内的元素 【经度 纬度 距离 单位】
2)实例
获取东经 110 北纬 30 方圆1000km以内的城市
```
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "chongqing"
2) "shenzhen"
```
## 七)Jedis操作Redis
### 7.1)Jedis工程创建
#### 7.1.1)新建Maven工程
新建Maven工程——jedis_redisdemo,在POM文件中引入Jedis所需要的jar包
```
redis.clients
jedis
3.2.0
```
连接Redis注意事项
禁用Linux的防火墙:Linux(CentOS7)里执行命令:
```
systemctl stop/disable firewalld.service
```
redis.conf中注释掉bind 127.0.0.1 ,然后 protected-mode 设置为 no,修改端口6379为 11079
#### 7.1.2)创建测试程序
##### 7.1.2.1) Jedis连通性测试
新建 JedisDemo1.java,编写测试代码:
```java
public class JedisDemo1 {
public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("82.157.183.197",11079);
//测试
String value = jedis.ping();
System.out.println(value);
jedis.close();
}
}
```
输出:
```
PONG
```
##### 7.1.2.2) Jedis 操作key string测试
```java
//Jedis 操作key string
@Test
public void demo1() {
//创建Jedis对象
Jedis jedis = new Jedis("82.157.183.197",11079);
//添加
jedis.set("name","lucy");
//获取
String name = jedis.get("name");
System.out.println(name);
//设置多个key-value
jedis.mset("k1","v1","k2","v2");
List mget = jedis.mget("k1", "k2");
System.out.println(mget);
Set keys = jedis.keys("*");
for(String key : keys) {
System.out.println(key);
}
jedis.close();
}
```
输出:
```
name:luck
mget:[v1, v2]
Redis中存在key:name
Redis中存在key:k1
Redis中存在key:k2
```
##### 7.1.2.3) Jedis 操作List 测试
```java
//Jedis 操作list
@Test
public void demo2() {
//创建Jedis对象
Jedis jedis = new Jedis("82.157.183.197", 11079);
jedis.lpush("key1", "lucy", "mary", "jack");
List values = jedis.lrange("key1", 0, -1);
System.out.println("Redis中存在List:" +values);
jedis.close();
}
```
输出:
```
Redis中存在List:[jack, mary, lucy]
```
##### 7.1.2.4)Jedis 操作Set 测试
```
//Jedis 操作set
@Test
public void demo3() {
//创建Jedis对象
Jedis jedis = new Jedis("82.157.183.197", 11079);
jedis.sadd("names", "lucy");
jedis.sadd("names", "mary");
Set names = jedis.smembers("names");
System.out.println("Redis中存在Set:" + names);
jedis.close();
}
```
输出:
```
Redis中存在Set:[lucy, mary]
```
##### 7.1.2.5)Jedis 操作Hash 测试
```
//Jedis 操作hash
@Test
public void demo4() {
//创建Jedis对象
Jedis jedis = new Jedis("82.157.183.197", 11079);
jedis.hset("users", "age", "20");
String hget = jedis.hget("users", "age");
System.out.println("Redis中存在Hash:" + hget);
jedis.close();
}
```
输出:
```
Redis中存在Hash:20
```
7.1.2.6)Jedis 操作Zset 测试
```
//Jedis 操作zset
@Test
public void demo5() {
//创建Jedis对象
Jedis jedis = new Jedis("82.157.183.197", 11079);
jedis.zadd("china", 100d, "shanghai");
Set china = jedis.zrange("china", 0, -1);
System.out.println("Redis中存在Zset:" + china);
jedis.close();
}
```
输出:
```
Redis中存在Zset:[shanghai]
```
### 7.2)Jedis实例-手机验证码
#### 7.2.1)功能要求
完成一个手机验证码功能:
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入3次
设计分析:

代码如下:
```java
public class PhoneCode {
public static void main(String[] args) {
// 测试步骤1 生成 成6位数字验证码
// String code = getCode();
// System.out.println(code);
// 输出:565496
//测试步骤2 模拟验证码发送
verifyCode("13678765435");
// 第一次测试输出:生成验证码 =561523
// 第二次测试输出:生成验证码 =376848
// 第三次测试输出:生成验证码 =983279
// 第四次测试输出:今天发送次数已经超过三次
//测试步骤3 模拟验证码校验
// getRedisCode("13678765435","561523");
// 第一次测试输出:成功
// getRedisCode("13678765435", "1234");
// 第二次测试输出:失败
// 过一会再次测试 第三次测试输出:验证码已过期,请重新生成成
}
//步骤1 生成6位数字验证码
public static String getCode() {
Random random = new Random();
String code = "";
for (int i = 0; i < 6; i++) {
int rand = random.nextInt(10);
code += rand;
}
return code;
}
//步骤2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120
public static void verifyCode(String phone) {
//连接redis
Jedis jedis = new Jedis("82.157.183.197", 11079);
//拼接key
//手机发送次数key
String countKey = "VerifyCode" + phone + ":count";
//验证码key
String codeKey = "VerifyCode" + phone + ":code";
//每个手机每天只能发送三次,发送次数为 count
String count = jedis.get(countKey);
if (count == null) {
//没有发送次数,第一次发送
//设置发送次数是1
jedis.setex(countKey, 24 * 60 * 60, "1");
} else if (Integer.parseInt(count) <= 2) {
// 发送次数为2,设置 发送次数+1
jedis.incr(countKey);
} else if (Integer.parseInt(count) > 2) {
//发送三次,不能再发送
System.out.println("今天发送次数已经超过三次");
jedis.close();
return;
}
//发送验证码放到redis里面
// 获取验证码
String vcode = getCode();
System.out.println("生成验证码 =" + vcode);
// 设置过期时间120秒,即2分钟
jedis.setex(codeKey, 120, vcode);
jedis.close();
}
//步骤3 验证码校验
public static void getRedisCode(String phone, String code) {
//从redis获取验证码
//连接redis
Jedis jedis = new Jedis("82.157.183.197", 11079);
//验证码key
String codeKey = "VerifyCode" + phone + ":code";
String redisCode = jedis.get(codeKey);
if (redisCode == null) {
System.out.println("验证码已过期,请重新生成");
} else if (redisCode.equals(code)) {
System.out.println("成功");
} else {
System.out.println("失败");
}
jedis.close();
}
}
```
第一次测试 verifyCode() 后,redis中内容如下图:

## 八)Redis与Spring Boot整合
### 8.1)新建SpingBoot工程——redis_springboot
直接在IDEA中创建,或者去网站 :https://start.spring.io/ 设置相关工程配置
### 8.2)修改工程POM文件
在pom.xml文件中引入redis相关依赖
```xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
com.study
redis_springboot
0.0.1-SNAPSHOT
redis_springboot
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
2.6.0
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
```
### 8.3)修改工程配置文件
application.properties添加redis配置
```properties
#Redis服务器地址
spring.redis.host=82.157.183.197
#Redis服务器连接端口
spring.redis.port=11079
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
```
### 8.4)添加redis配置类
工程目录下新建config文件夹,并新增redis配置类 RedisConfig.java【固定写法】
```java
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
RedisSerializer redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
```
### 8.5)添加redis接口测试类
工程目录下新建controller文件夹,并新增redis接口测试类类 RedisTestController.java
```
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","luck");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
```
#### 8.6)测试
启动工程,浏览器访问:http://127.0.0.1:8080/redisTest
浏览器返回结果:luck 【给 Redis 赋值的内容】
## 九)Redis6的事务操作
### 9.1)Redis的事务定义
Redis事务是一个**单独的隔离操作**:【事务操作是相互隔离的,其他操作不能插入到当前操作中】
- 事务中的所有命令都会序列化、按顺序地执行;
- 事务在执行的过程中,不会被其他客户端发送来的命令请求所打断;
- 主要作用就是**串联多个命令防止别的命令插队**
### 9.2)Redis的事务操作基本命令
#### 9.2.1)事务操作的过程
1. 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行;
2. 直到输入Exec后,Redis会将之前的命令队列中的命令依次执行;
3. 组队的过程中可以通过discard来放弃组队
事务操作的过程如下图:

#### 9.2.2)事务操作的实例
展示 multi 和 exec
```
tx-root-redis:0>multi
"OK"
tx-root-redis:0>set k1 v1
"QUEUED"
tx-root-redis:0>set k2 v2
"QUEUED"
tx-root-redis:0>exec
1) "OK"
2) "OK"
```
展示 multi 和 discard
```
tx-root-redis:0>set a1 s1
"QUEUED"
tx-root-redis:0>set a2 s2
"QUEUED"
tx-root-redis:0>discard
"OK"
```
#### 9.2.3)事务的错误处理
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消

案例展示:
```
tx-root-redis:0>multi
"OK"
tx-root-redis:0>set b1 v1
"QUEUED"
tx-root-redis:0>set b2 v2
"QUEUED"
tx-root-redis:0>set b3
"ERR wrong number of arguments for 'set' command"
tx-root-redis:0>exec
"EXECABORT Transaction discarded because of previous errors."
```
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚

案例展示:
```
tx-root-redis:0>multi
"OK"
tx-root-redis:0>set c1 v1
"QUEUED"
tx-root-redis:0>incr c1
"QUEUED"
tx-root-redis:0>set c2 v2
"QUEUED"
tx-root-redis:0>exec
1) "OK"
2) "ERR value is not an integer or out of range"
3) "OK"
```
#### 9.2.4)事务冲突的问题
初始金额为:10000
三个请求:
一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000
请求过程如下图:

没有添加事务管理导致金额出现问题,事务管理是通过给事务添加锁的机制来实现的,锁分为悲观锁和乐观锁
#### 9.2.5)事务冲突解决——悲观锁
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁;
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁;
结合案例,悲观锁的执行流程如下图:

每一次操作数据前先上锁,其他线程处于阻塞状态,不符合条件就不在执行
优点:可以解决上述出现的事务问题
缺点:效率低,只能单线程进行操作,不能多线程同时进行
#### 9.2.6)事务冲突解决——乐观锁
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制;
乐观锁适用于多读的应用类型,这样可以提高吞吐量;【抢票场景】
Redis就是利用这种check-and-set机制实现事务的;
结合案例,乐观锁的执行流程如下图:

每一次操作时,给要修改的字段添加版本号,所有线程都能收到该版本信息V1.0,第一个线程执行结束后,金额从 10000变成了2000,同时在更新金额时也要同步更新版本信息V1.0为V1.1,其他行程执行完成会判断当前该字段的版本信息【V1.1】是否跟数据库版本信息一致【V1.0】,不一致则不能执行该线程的修改操作
#### 9.2.7)Redis中使用乐观锁
##### 9.2.7.1)WATCH key [key ...]
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
案例展示:使用两个客户端分别连接Redis
客户端1
```
127.0.0.1:11079> get balance
"100"
127.0.0.1:11079> watch balance
OK
127.0.0.1:11079> multi
OK
127.0.0.1:11079(TX)> incrby balance 10
QUEUED
127.0.0.1:11079(TX)> exec
1) (integer) 110
127.0.0.1:11079> get balance
"110"
```
客户端2
```
127.0.0.1:11079> get balance
"100"
127.0.0.1:11079> watch balance
OK
127.0.0.1:11079> multi
OK
127.0.0.1:11079(TX)> incrby balance 20
QUEUED
127.0.0.1:11079(TX)> exec
(nil)
127.0.0.1:11079> get balance
"110"
```
在客户端1对balance进行+10操作后,客户端2对balance进行+20的操作就不再生效了
##### 9.2.7.2)unwatch
取消 WATCH 命令对所有 key 的监视
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH了
官网文档:http://doc.redisfans.com/transaction/exec.html
##### 9.2.7)Redis事务三特性
- 单独的隔离操作 :事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断 ;
- 没有隔离级别的概念: 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性 :事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
### 9.3)Redis的事务秒杀案例
#### 9.3.1)解决计数器和人员记录的事务操作
秒杀主要包括两个操作:1)商品库存 - 1 2)秒杀成功的该用户加到秒杀成功者清单里

#### 9.3.2)不考虑并发的秒杀案例实现
新建工程 Seckill ,这是一个WEB工程
创建页面:index.jsp
```jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
Insert title here
iPhone 13 Pro !!! 1元秒杀!!!
```
创建 SecKillServlet,页面按钮点击秒杀调用doPost()方法
```java
public class SecKillServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public SecKillServlet() {
super();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 通过随机数生成用户id
String userid = new Random().nextInt(50000) + "";
// 接收调用的商品id
String prodid = request.getParameter("prodid");
// 调用秒杀方法
boolean isSuccess = SecKill_redis.doSecKill(userid, prodid);
// 根据秒杀的返回结果返回相应信息
response.getWriter().print(isSuccess);
}
}
```
创建 SecKill_redis.java,秒杀主方法,秒杀的全流程在这里开发
```java
public class SecKill_redis {
public static void main(String[] args) {
Jedis jedis = new Jedis("82.157.183.197", 11079);
System.out.println(jedis.ping());
jedis.close();
}
//秒杀主方法--全过程
public static boolean doSecKill(String uid, String prodid) throws IOException {
//步骤1 根据入参用户ID-uid和商品ID-prodid 进行非空判断
if (uid == null || prodid == null) {
return false;
}
//步骤2 连接redis
Jedis jedis = new Jedis("82.157.183.197", 11079);
//步骤3 拼接key
// 3.1 库存key
String kcKey = "sk:" + prodid + ":qt";
// 3.2 秒杀成功的用户key
String userKey = "sk:" + prodid + ":user";
// 步骤4 获取库存,如果库存null,秒杀还没有开始
String kc = jedis.get(kcKey);
if (kc == null) {
System.out.println("秒杀还没有开始,请等待");
jedis.close();
return false;
}
// 步骤5 判断用户是否重复秒杀操作
// 判断set集合中的用户信息,因为不能存在重复的用户【不能重复秒杀】所以使用Set存储用户信息
if (jedis.sismember(userKey, uid)) {
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
// 步骤6 判断如果商品数量,库存数量小于1,秒杀结束
if (Integer.parseInt(kc) <= 0) {
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
// 步骤7 秒杀的过程
//7.1 库存-1
jedis.decr(kcKey);
//7.2 把秒杀成功用户添加清单里面
jedis.sadd(userKey, uid);
System.out.println("秒杀成功了..");
jedis.close();
return true;
}
}
```
测试:启动Tomcat工程
浏览器访问:http://localhost:8080/Seckill/
点击 “秒杀按钮” ,此时还未向 Redis 中添加 商品信息,效果如下图:

向Redis中加入商品信息,命令如下:【添加了5个商品】
```shell
127.0.0.1:11079> flushdb
OK
127.0.0.1:11079> keys *
(empty array)
127.0.0.1:11079> set sk:0101:qt 5
OK
127.0.0.1:11079> keys *
1) "sk:0101:qt"
```
刷新页面后再次点击秒杀按钮”,效果如下图:

弹框不再弹出,且后台提示 —— 秒杀成功了..
在Redis中查看相关商品和用户信息:
```
127.0.0.1:11079> get sk:0101:qt
"4"
127.0.0.1:11079> smembers sk:0101:user
1) "452"
```
发现商品数量已减少1,用户列表新增了秒杀成功的用户
再次点击秒杀按钮”,直到第六次,效果如下图:

弹框再次弹出,且后台提示 —— 秒杀已经结束了
在Redis中查看相关商品和用户信息:
```
127.0.0.1:11079> get sk:0101:qt
"0"
127.0.0.1:11079> smembers sk:0101:user
1) "452"
2) "16928"
3) "27602"
4) "41023"
5) "41347"
```
发现商品信息已经为0,同时用户列表中已经有了5个秒杀成功的用户
上述方案没有考虑并发,会存在问题
**并发测试:**
在Linux系统中安装:
```
yum install httpd-tools
```
通过ab进行测试
在根目录新建 postfile 文件,内容如下:
```
prodid=0101&
```
在根目录执行下列命令,执行并发测试
```
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://172.19.83.237:8080/Seckill/doseckill
```
【1000个请求、100个并发,调用172.19.83.237(个人主机)上的秒杀方法】
输出:

存在问题:
- 【超卖问题】已经秒杀结束了,但还存在秒杀成功的情况;Redis 中的商品信息为 -3,不能出现商品为负数的情况
- 【连接超时问题】Redis 无法同时处理过多的请求,不能处理的请求需要等待,等待时间过长会报连接超时错误
#### 9.3.3)并发的秒杀案例实现
上述因没有考虑并发,而存在的连接超时问题和超卖问题解决方案如下:
##### 9.3.3.1)连接超时,通过连接池解决
连接池:
- 节省每次连接redis服务带来的消耗,把连接好的实例反复利用;
- 通过参数管理连接的行为
连接池参数:
- MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted;
- maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
- MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
- testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
Redis连接池配置类:JedisPoolUtil.java
```java
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
// 连接池资源连接方法
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100 * 1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true); // ping PONG
jedisPool = new JedisPool(poolConfig, "82.157.183.197", 11079, 60000);
}
}
}
return jedisPool;
}
// 连接池资源释放方法
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}
}
```
修改上述 Redis接口实现类 SecKill_redis.java 中 连接redis的方法为通过连接池得到jedis对象
```java
//步骤2 连接redis
// Jedis jedis = new Jedis("82.157.183.197", 11079);
// 步骤2 通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
```
##### 9.3.3.2)超卖问题
超卖问题:已经秒杀结束了,但还存在秒杀成功的情况;Redis 中的商品信息为 -3,不能出现商品为负数的情况

解决方案:利用乐观锁淘汰用户,解决超卖问题

修改上述 Redis接口实现类 SecKill_redis.java 中 秒杀的过程添加乐观锁事务管理
```java
//秒杀主方法--全过程
public static boolean doSecKill(String uid, String prodid) throws IOException {
//步骤1 根据入参用户ID-uid和商品ID-prodid 进行非空判断
if (uid == null || prodid == null) {
return false;
}
//步骤2 连接redis
// Jedis jedis = new Jedis("82.157.183.197", 11079);
// 步骤2 通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//步骤3 拼接key
// 3.1 库存key
String kcKey = "sk:" + prodid + ":qt";
// 3.2 秒杀成功的用户key
String userKey = "sk:" + prodid + ":user";
// 秒杀过程添加乐观锁事务管理 -- 监视库存
jedis.watch(kcKey);
// 步骤4 获取库存,如果库存null,秒杀还没有开始
String kc = jedis.get(kcKey);
if (kc == null) {
System.out.println("秒杀还没有开始,请等待");
jedis.close();
return false;
}
// 步骤5 判断用户是否重复秒杀操作
// 判断set集合中的用户信息,因为不能存在重复的用户【不能重复秒杀】所以使用Set存储用户信息
if (jedis.sismember(userKey, uid)) {
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
// 步骤6 判断如果商品数量,库存数量小于1,秒杀结束
if (Integer.parseInt(kc) <= 0) {
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
/* // 步骤7 秒杀的过程
//7.1 库存-1
jedis.decr(kcKey);
//7.2 把秒杀成功用户添加清单里面
jedis.sadd(userKey, uid);*/
//步骤7改造 秒杀过程添加乐观锁事务管理
//使用事务
Transaction multi = jedis.multi();
//组队操作
multi.decr(kcKey);
multi.sadd(userKey,uid);
//执行
List