Redis学习笔记
一、Redis基本安装
wget https://download.redis.io/releases/redis-6.2.3.tar.gz
tar -zxvf redis-6.2.3.tar.gz
yum install gcc
cd redis-6.2.3/
make
make install
如果安装报下面这个错,先执行一下这个命令make distclean
,再进行安装即可:
默认安装目录在/usr/local/bin
:
设置后台启动
mkdir -p /opt/module/redis
cp ~/soft/redis-6.2.3/redis.conf /opt/module/redis/
vim /opt/module/redis/redis.conf
修改内容:
daemonize yes
#bind 127.0.0.1 -::1
protected-mode no
上面修改成后台启动、运行远程访问、允许非本机访问。
启动:
redis-server /opt/module/redis/redis.conf
连接:
redis-cli -p 6379
二、五种常用数据类型
2.1 redis key
keys * 查看当前库所有key (匹配:keys *1)
exists key 判断某个key是否存在
type key 查看你的key是什么类型
del key 删除指定的key数据
unlink key 根据value选择非阻塞删除 仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。
expire key 10 10秒钟:为给定的key设置过期时间
ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
select 命令切换数据库
dbsize 查看当前数据库的key的数量
flushdb 清空当前库
flushall 通杀全部库
2.2 string
String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
set <key><value> 添加键值对
*NX:当数据库中key不存在时,可以将key-value添加数据库
*XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
*EX:key的超时秒数
*PX:key的超时毫秒数,与EX互斥
get <key> 查询对应键值
append <key><value> 将给定的<value> 追加到原值的末尾
strlen <key> 获得值的长度
setnx <key><value> 只有在 key 不存在时 设置 key 的值
incr <key> 将 key 中储存的数字值增1,只能对数字值操作,如果为空,新增值为1
decr <key> 将 key 中储存的数字值减1,只能对数字值操作,如果为空,新增值为-1
incrby/decrby <key><步长> 将 key 中储存的数字值增减。自定义步长。
mset <key1><value1><key2><value2> ..... 同时设置一个或多个 key-value对
mget <key1><key2><key3> ..... 同时获取一个或多个 value
msetnx <key1><value1><key2><value2> ..... 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。原子性,有一个失败则都失败
getrange <key><起始位置><结束位置> 获得值的范围,类似java中的substring,前包,后包
setrange <key><起始位置><value> 用 <value> 覆写<key>所储存的字符串值,从<起始位置>开始(索引从0开始)。
setex <key><过期时间><value> 设置键值的同时,设置过期时间,单位秒。
getset <key><value> 以新换旧,设置了新值同时获得旧值。
数据结构
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。
字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
2.3 list
单键多值
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
lpush/rpush <key><value1><value2><value3> .... 从左边/右边插入一个或多个值。(和栈类似,从左边/右边的值依次push)
lpop/rpop <key> 从左边/右边吐出一个值。值在键在,值光键亡。
rpoplpush <key1><key2> 从<key1>列表右边吐出一个值,插到<key2>列表左边。
lrange <key><start><stop> 按照索引下标获得元素(从左到右)
lrange mylist 0 -1 0左边第一个,-1右边第一个,(0-1表示获取所有)
lindex <key><index> 按照索引下标获得元素(从左到右)
llen <key> 获得列表长度
linsert <key> before <value><newvalue> 在<value>的后面插入<newvalue>插入值
lrem <key><n><value> 从左边删除n个value(从左到右)
lset<key><index><value> 将列表key下标为index的值替换成value
数据结构(这块如果不太理解,就看一下尚硅谷的redis视频讲解)
List的数据结构为快速链表quickList
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。
它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
2.4 set
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
sadd <key><value1><value2> ..... 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
smembers <key> 取出该集合的所有值。
sismember <key><value> 判断集合<key>是否为含有该<value>值,有1,没有0
scard<key> 返回该集合的元素个数。
srem <key><value1><value2> .... 删除集合中的某个元素。
spop <key> 随机从该集合中吐出一个值。
srandmember <key><n> 随机从该集合中取出n个值。不会从集合中删除 。
smove <source><destination>value 把集合中一个值从一个集合移动到另一个集合
sinter <key1><key2> 返回两个集合的交集元素。
sunion <key1><key2> 返回两个集合的并集元素。
sdiff <key1><key2> 返回两个集合的差集元素(key1中的,不包含key2中的)
数据结构
Set数据结构是dict字典,字典是用哈希表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
2.5 hash
Redis hash 是一个键值对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
类似Java里面的Map<String,Object>
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储
主要有以下2种存储方式:
![](https://img2020.cnblogs.com/blog/1821653/202105/1821653-20210513041307482-1766038652.png)
hset <key><field><value> 给<key>集合中的<field>键赋值<value>
hget <key1><field> 从<key1>集合<field>取出 value
hmset <key1><field1><value1><field2><value2>... 批量设置hash的值
hexists<key1><field> 查看哈希表 key 中,给定域 field 是否存在。
hkeys <key> 列出该hash集合的所有field
hvals <key> 列出该hash集合的所有value
hincrby <key><field><increment> 为哈希表 key 中的域 field 的值加上增量 1 -1
hsetnx <key><field><value> 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .
数据结构
Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
2.6 zset
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
zadd <key><score1><value1><score2><value2>… 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
zrange <key><start><stop> [WITHSCORES] 返回有序集 key 中,下标在<start><stop>之间的元素 带WITHSCORES,可以让分数一起和值返回到结果集。
zrangebyscore key minmax [withscores] [limit offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
zrevrangebyscore key maxmin [withscores] [limit offset count] 同上,改为从大到小排列。
zincrby <key><increment><value> 为元素的score加上增量
zrem <key><value> 删除该集合下,指定值的元素
zcount <key><min><max> 统计该集合,分数区间内的元素个数
zrank <key><value> 返回该值在集合中的排名,从0开始。
案例:如何利用zset实现一个文章访问量的排行榜?
192.168.66.62:6380> zadd topn 1000 v1 2000 v2 3000 v3
-> Redirected to slot [15087] located at 192.168.66.63:6379
(integer) 3
192.168.66.63:6379> zrevrange topn 0 9 withscores
1) "v3"
2) "3000"
3) "v2"
4) "2000"
5) "v1"
6) "1000"
基础演示:
![](https://img2020.cnblogs.com/blog/1821653/202104/1821653-20210414134057790-1730950481.png)
![](https://img2020.cnblogs.com/blog/1821653/202104/1821653-20210414134415050-1114094955.png)
![](https://img2020.cnblogs.com/blog/1821653/202104/1821653-20210414135132428-1221586525.png)
Redis主从复制
mkdir /opt/module/redis
复制redis.conf
配置一主节两重
Redis集群
存在的问题
-
容量不够,redis如何进行扩容?
-
并发写操作, redis如何分摊?
-
另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。
之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。
什么是无中心化,任何一台主机都能作为集群入口。主机之间可以相互连通:例如某个请求,不是订单主机处理,订单可以转给用户主机处理,用户可以转给商品主机处理:
什么是集群
-
Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
-
Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
搭建redis集群
我这边使用三台linux进行搭建,每台使用6379、6380端口安装两个redis服务,总共有6个redis节点,3主3从。
首先,需要先完成Redis的基本安装,具体内容前面已经介绍过了。
1、node01节点的配置:
下面这两个配置文件的添加,在node01节点执行即可,最后会复制到node02、node03节点。
vim /opt/module/redis/redis_6379.conf
配置内容:
include /opt/module/redis/redis.conf
pidfile "/var/run/redis_6379.pid"
port 6379
dbfilename "dump6379.rdb"
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
再添加一个6380的配置:配置内容都是类似
vim /opt/module/redis/redis_6380.conf
配置内容:
include /opt/module/redis/redis.conf
pidfile "/var/run/redis_6380.pid"
port 6380
dbfilename "dump6380.rdb"
cluster-enabled yes
cluster-config-file nodes-6380.conf
cluster-node-timeout 15000
2、node02、node03需要做的事情:
在node02、node03进行redis的基础安装。
3、将node01的配置文件复制到node02、node03:
[root@node01 redis]# scp -r /opt/module/redis/ root@node02:/opt/module/redis
[root@node01 redis]# scp -r /opt/module/redis/ root@node03:/opt/module/redis
4、启动6个redis服务,在三台都执行
redis-server /opt/module/redis/redis_6379.conf
redis-server /opt/module/redis/redis_6380.conf
ps -ef | grep redis
如果每台机器都是下面这样的cluster和nodes-xxx,说明配置没问题。
5、将6个节点连接成一个集群
在node01 /root/soft/redis-6.2.3/src
下执行:
redis-cli --cluster create --cluster-replicas 1 192.168.66.61:6379 192.168.66.61:6380 192.168.66.62:6379 192.168.66.62:6380 192.168.66.63:6379 192.168.66.63:6380
6、连接集群(需要加-c参数)
redis-cli -c -p 6379
127.0.0.1:6379> cluster nodes
必须要出现下面的这6个节点信息才行哦,否则就参考这篇博客重新处理一下集群:https://blog.csdn.net/justry_deng/article/details/89205155
如何分配6个节点?
一个集群至少要有三个主节点。
选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。
插槽
-
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个,
-
集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
-
集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:
节点 A 负责处理 0 号至 5460 号插槽。
节点 B 负责处理 5461 号至 10922 号插槽。
节点 C 负责处理 10923 号至 16383 号插槽。
如果不在当前插槽,会进行重定向:
192.168.66.63:6379> set k2 v2
-> Redirected to slot [449] located at 192.168.66.61:6379
OK
不支持多值插入:因为两个key的插槽不一样
192.168.66.61:6379> mset k1 v1 k2 v2
(error) CROSSSLOT Keys in request don't hash to the same slot
如果实在想插入多个键值,需要使用分组:
192.168.66.61:6379> mset k1{group01} v1 k2{group01} v2
-> Redirected to slot [7544] located at 192.168.66.62:6379
OK
根据key获取插槽的值:
192.168.66.62:6379> cluster keyslot group01
(integer) 7544
获取某个插槽中key的数量:
192.168.66.62:6379> cluster countkeysinslot 7544
(integer) 2
获取某个插槽中的keys:
CLUSTER GETKEYSINSLOT
192.168.66.62:6379> cluster getkeysinslot 7544 10
1) "k1{group01}"
2) "k2{group01}"
故障恢复
先停掉一台主节点:
echo shutdown | redis-cli -p 6379 --pipe
连上这台的6380节点,查看集群信息:
[root@node01 src]# redis-cli -c -p 6380
127.0.0.1:6380> cluster nodes
发现上面的61机器的6379已经是disconnected了。
再次启动6379,观察状态:
redis-server /opt/module/redis/redis_6379.conf
突然发现,我重新启动了好多次,都是不能够加入集群了
说实话,慌得一批,后来找到一篇博客:https://blog.csdn.net/Ntozot/article/details/103910253
最最有可能的原因,redis-server 启动时 的路径不对,一定要在你之前启动的目录下面,也就是dump.rdb,nodes.conf等等这些文件所在的目录,因为读取不到cluster-config-file文件最可能因为路径不对找不到文件,在哪个路径执行启动命令,这些文件就生成在哪,除非你redis.conf文件里面配置的是绝对路径。
我在这个路径下执行启动redis就好了。一定要注意哈。
[root@node01 redis]# redis-server /opt/module/redis/redis_6379.conf
[root@node01 redis]# pwd
/opt/module/redis
启动之后,6389已经是从机了,不再是主节点:
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?
-
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉
-
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。
redis.conf中的参数 cluster-require-full-coverage
集群Jedis开发
即使连接的不是主机,集群会自动切换主机存储。主机写,从机读。
无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据。
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort>set =new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.31.211",6379));
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));
}
}
集群优缺点
Redis 集群提供了以下好处
-
实现扩容
-
分摊压力
-
无中心配置相对简单
Redis 集群的不足
-
多键操作是不被支持的
-
多键的Redis事务是不被支持的。lua脚本不被支持
-
由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。
springboot整合redis集群
整合jedis
导入依赖(我们这里使用的是jedis客户端,所以要排除lettuce,默认使用的是lettuce)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--使用jedis作为客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
配置文件(参考:https://www.cnblogs.com/telwanggs/p/10826887.html)
spring:
redis:
cluster:
nodes:
- 192.168.66.61:6379
- 192.168.66.61:6380
- 192.168.66.62:6379
- 192.168.66.62:6380
- 192.168.66.63:6379
- 192.168.66.63:6380
max-redirects: 3 #获取slot失败,最大重定向次数
jedis:
pool:
max-active: 1000 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 11 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
控制层类
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("test")
public void redisTest(){
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("k1", "twx857602");
ops.set("k2", "6666");
}
短信验证码实现
# 自定义redis key
redis:
key:
prefix:
authCode: "portal:authCode:"
expire:
authCode: 120 # 验证码超期时间
抽样redis一些常用操作
public interface RedisService {
/**
* 存储数据
*/
void set(String key, String value);
/**
* 获取数据
*/
String get(String key);
/**
* 设置超期时间
*/
boolean expire(String key, long expire);
/**
* 删除数据
*/
void remove(String key);
/**
* 自增操作
* @param delta 自增步长
*/
Long increment(String key, long delta);
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void set(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public boolean expire(String key, long expire) {
return stringRedisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
@Override
public void remove(String key) {
stringRedisTemplate.delete(key);
}
@Override
public Long increment(String key, long delta) {
return stringRedisTemplate.opsForValue().increment(key,delta);
}
}
上面两个类可以作为工具类使用,抽象出来。
业务实现
/**
* 短信验证码接口
*/
public interface SmsService {
/**
* 生成验证码
*/
String generateAuthCode(String telephone);
/**
* 判断验证码和手机号码是否匹配
*/
Boolean verifyAuthCode(String telephone, String authCode);
}
@Service
public class SmsServiceImpl implements SmsService{
@Autowired
private RedisService redisService;
@Value("${redis.key.prefix.authCode}")
private String REDIS_KEY_PREFIX_AUTH_CODE;
@Value("${redis.key.expire.authCode}")
private Long AUTH_CODE_EXPIRE_SECONDS;
@Override
public String generateAuthCode(String telephone) {
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 6; i++) {
sb.append(random.nextInt(10));
}
//验证码绑定手机号并存储到redis
redisService.set(REDIS_KEY_PREFIX_AUTH_CODE + telephone, sb.toString());
redisService.expire(REDIS_KEY_PREFIX_AUTH_CODE + telephone, AUTH_CODE_EXPIRE_SECONDS);
return sb.toString();
}
@Override
public Boolean verifyAuthCode(String telephone, String authCode) {
if (StringUtils.isEmpty(authCode)) {
return false;
}
String realAuthCode = redisService.get(REDIS_KEY_PREFIX_AUTH_CODE + telephone);
return authCode.equals(realAuthCode);
}
}
@RestController
public class SmsController {
@Autowired
private SmsService smsService;
@RequestMapping(value = "/getAuthCode", method = RequestMethod.GET)
public String getAuthCode(@RequestParam String telephone) {
return smsService.generateAuthCode(telephone);
}
@RequestMapping(value = "/verifyAuthCode", method = RequestMethod.POST)
public Boolean verifyAuthCode(@RequestParam String telephone, @RequestParam String authCode) {
return smsService.verifyAuthCode(telephone,authCode);
}
}