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种存储方式:

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"

基础演示:

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 返回 count 个 slot 槽中的键

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);
    }
}

访问:http://localhost:8080/getAuthCode?telephone=17611219021

posted @ 2021-05-12 05:25  沉淀技术这十年  阅读(146)  评论(0编辑  收藏  举报