Redis

redis

目录

一、Linux 安装 Redis(5.0.14)

1. 安装 wget 命令

 2. 安装 redis 所依赖的环境

3. 获取网络安装包

4. 解压安装包

5. 编译,进入 redis 解压目录,执行 make 命令编译

6.安装

7.启动 redis

8. 修改配置文件

设置密码

设置远程连接

启动 redis

9. 简单使用

二.Key

三、Redis的五种基本数据类型

1、字符串(String)

2、列表(List)

3、集合(Set)

4、哈希(Hash)

5、有序集合(Zset)

四、发布订阅

五、事务

1. 基本命令

2. 有关事务,经常会遇到的是两类错误:

3. 悲观锁

4. 乐观锁

5. WATCH

6. 事务回滚:

六、Java 操作 Redis

七、开启 Redis 缓存

1. yml配置文件 application.yml

2. 添加缓存的bean

3. 配置文件上面加开启缓存的注解

4. 对应需要加缓存的地方加上缓存

八、基于注解的支持

@Cacheable

@CachePut

@CacheEvict

1、 allEntries属性

2、 beforeInvocation属性

九、在项目中的应用

1. 实体类Dept

2. DeptMapper

3. IDeptSerice

4. DeptServiceImpl

5. DeptController

十、验证码

1. 配置文件

2. service层

3. 控制层

4. 效果

十一、秒杀

1. 在控制层编写秒杀相关的后台代码

2. 创建商品,存放到Redis中

3. 启动 Apache JMeter  创建线程组

4. 设置100个线程来模拟100人

5. 添加HTTP请求

6. 启动请求

十二、Redis持久化之RDB

1.RDB是什么

2. 备份是如何执行的

3. Fork

4. RDB持久化流程

5. dump.rdb文件

6. 配置位置

7. 如何触发RDB快照;保持策略

7.1 配置文件中默认的快照配置时间间隔​编辑

7.2 命令save bgsave

​编辑

7.3 flushall命令

7.4 SNAPSHOTTING快照

7.5. Save

7.6. stop-writes-on-bgsave-error

​编辑

7.7 rdbcompression 压缩文件

​编辑

7.8. rdbchecksum 检查数据的完整性

​编辑

7.9. rdb的备份

7.10. 优势

7.11 劣势

十三、Redis持久化之AOF

1. AOF(Append Only File)

1.1 是什么

1.2. AOF持久化流程

1.3. AOF默认不开启

​编辑

1.4. AOF和RDB同时开启,redis听谁的?

1.5. AOF启动/修复/恢复

1.6. AOF同步频率设置

1.7. Rewrite压缩

1. 概念

2. 重写原理,如何实现重写

3. 触发机制,何时重写

4. 重写流程(背)

2. 优劣分析

2.1 优势

2.2 劣势

2.3 用哪个好

2.4 官方建议

十四、主从复制

1. 是什么

2. 能干嘛

3 主从复制

一主二仆

启动三台redis服器

查看三台服务器的运行情况

复制原理

薪火相传

反客为主

哨兵模式(sentinel)

十五、集群搭建

1、应用场景

2、基本原理

3、主从复制的作用

4、配置集群所需的环境

1、规划网络。

2、创建 Redis 节点

3、创建目录

4、在7001目录下配置redis.conf

5、将7001目录下 redis.conf 拷贝到其他这五个目录中

​编辑

6、配置一键启动这6个redis

7、创建redis的集群

8、使用cli连接redis集群

​编辑

9、检查集群的状态

10、添加主节点

11、配置从节点

12、删除节点

12.1 删除从节点:

​编辑

12.2 删除主节点

13、批量添加

​编辑

十六、什么是slots

1. 在集群中录入值

2. Redis 集群优点

3. Redis 集群的不足

十七、分布式锁

问题描述

解决方案:使用redis实现分布式锁

优化之设置锁的过期时间

优化之UUID防误删

LUA脚本保证删除的原子性

lUA脚本

LUA脚本在Redis中的优势

redisson

使用redissoncheck

锁的分类:

Redission的使⽤

使用集群


        REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。

        Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。

        Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。

一、Linux 安装 Redis(5.0.14)

这里选择安装到自己创建的 /usr/redis 目录下

1. 安装 wget 命令

yum -y install wget

 2. 安装 redis 所依赖的环境

yum -y install gcc-c++

3. 获取网络安装包

 wget https://download.redis.io/releases/redis-5.0.14.tar.gz

4. 解压安装包

tar -zxvf redis-5.0.14.tar.gz

5. 编译,进入 redis 解压目录,执行 make 命令编译

6.安装

make PREFIX=/usr/redis install

 (如果不加 prefix 将默认安装到 /usr/local 目录下面)

(如果命令执行不成功,换为 make install PREFIX=/usr/redis )

7.启动 redis

        进入安装目录(/usr/redis)的 bin 下面,并启动 redis

./reids-service

8. 修改配置文件

拷贝配置文件到安装目录的 bin 下

 cp /usr/redis/redis-5.0.14/redis.conf /usr/redis/bin

修改 redis.conf 文件

设置后台启动

no 改为 yes #如果不想设置后台启动就不改

设置密码

requirepass 密码   #配置密码 如果需要 客户端连接的话需要设置密码 如果不需要连接就不要设置

设置远程连接

# bind 127.0.0.1 #注释掉绑定本机,才可以远程连接访问

启动 redis

./redis-server ./redis.conf

连接redis

关闭 redis:

./redis-cli shutdown

./redis-cli -p 6379 -a gcf -h 127.0.0.1 shutdown

配置服务启动(使用 systemctl 的方法)

服务启动的时候 daemonize 改为 no

在/lib/systemd/system 目录下创建一个脚本文件 redis.service,里面的内容如下:

[Unit]
Description=redis
After=network.target
[Service]
Type=forking
ExecStart=/usr/redis/bin/redis-server /usr/redis/bin/redis.conf 
ExecStop=/usr/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a 密码 shutdown
PrivateTmp=true
[Install]
WantedBy=multi-user.target

刚刚配置的服务需要让 systemctl 能识别,就必须刷新配置

systemctl daemon-reload  刷新配置
systemctl enable redis  开机自启
systemctl status redis   redis 状态
systemctl start redis  开启 redis
systemctl stop redis 关闭 redis
systemctl disable redis 禁止开机自启

远程连接:

redis-cli -h host -p port -a password 远程连接 redis 服务

ping 测试连接

quit 关闭连接(connection)

auth 简单密码认证

9. 简单使用

查询数据库一共有多少:

config get databases

选中某一个数据库

设值,取值

获取所有的 key

二.Key

默认 16 个数据库,类似数组下标从 0 开始,初始默认使用 0 号库

dbsize 查看当前数据库的 key 的数量

flushdb 清空当前库

flushall 通杀全部库

keys * 获取当前库中的所有key

select 0 选择第一个库

move key 1 将当前的数据库 key 移动到某个数据库,目标库有,则不能移动

randomkey 从当前数据库中随机返回

type key 类型

del key 删除 key

exists key 判断是否存在 key

expire key 10 为给定的key设置过期时间 单位是秒

pexpire key 1000 给定的key设置过期时间 单位:毫秒

persist key 删除key的过期时间

ttl key 查看还有多少秒过期,-1 表示永不过期,-2 表示已过期

三、Redis的五种基本数据类型

1、字符串(String)

string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。

string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。

string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。

set <key><value>  添加键值对
 
get <key>  查询对应键值
 
append <key><value>  将给定的value追加到原值的末尾
 
strlen <key>  获得值的长度
 
setnx <key><value>  只有key不存在时,设置key值
 
incr <key>  将key中储存的数字值增1,只能对数字值操作,如果为空,新增值为1
 
decr <key>  将key中储存的数字值建减1,只能对数字值操作,如果为空,新增值为-1
 
incrby / decrvy <key><步长>  将key中储存的数字值增减,自定义步长
 
mset <key1><value1><key2><value2>……  同时设置一个或多个 key-value 对
 
mget <key1><key2><key3>……  同时获取一个或多个value
 
msetnax  <key1><value1><key2><value2>……  同时设置一个或多个key-value对,当且仅当所有给定key都不存在
 
getrange <key><起始位置><结束位置>  获得值的范围,类似java中的substring,前包,后包
 
setrange <key><起始位置><value>  用<value>覆写<key>所存储的字符串值,从起始位置开始(索引从0开始)
 
setex <key><过期时间><value>  设置键值的同时,设置过期时间(单位:秒)
 
getset <key><value>  以新换旧,设置了新值的同时获得旧值

2、列表(List)

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

lpush / rpush <key><value1><value2><value3>……  从左边/右边插入一个或多个值
 
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个vlaue(从左到右)
 
lset <key><index><value>  将列表key下标为index的值替换成value

3、集合(Set)

Redis 的 Set 是 string 类型的无序集合。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

sadd <key><value1><value2>……	将一个或多个member元素加入到集合key中,已经存在的member元素将被忽略
 
smembers <key>	取出该集合的所有值
 
sismember <key><value>	判断集合<key>是否为该<value>值,有1,没有0
 
scard <key>	返回该集合的元素个数

srem <key><value1><valu2>……	删除集合中的某个元素
 
spop <key>	随机从该集合中吐出一个值
 
srandmember <key><n>	随机从该集合中取出n个值,不会从集合中删除
 
smove <source><destination>value	把集合中的一个值从一个集合移动到另一个集合


sinter<key1><key2>	返回两个集合的交集元素
 
sunion <key1><key2>	返回两个集合的并集元素
 
sdiff <key1><key2>	返回两个集合的差集元素

4、哈希(Hash)

Redis hash 是一个键值(key=>value)对集合。

Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

hset <key><field><value>  给<key>结合中的<filed>键赋值<value>
 
hget <key1><field>  从<key1>集合<field>取出value
 
hmset <key1><field1><value1><field2><value2>……  批量设置hash的值
 
hexits <key1><filed>  查看哈希表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不存在

5、有序集合(Zset)

Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

zadd <key><score1><value1><score2><value2>……  将一个或多个member元素机器score值加入到有序集key中
 
zrange <key><start><stop> [WITHSCORES]  返回有序集key中,下标在strart到stop之间的元素(带WITHSCORES,可以让分数一起返回)
 
zrangebyscore key minmax [withscores][limit offset count]  返回有序集key中,所有score值介于min和max之间的成员(从小到大)
 
zrevrangebyscore key maxmin [withscores][limit offet count]  同上,从大到小排序
 
zincrby <key><increment><value>  为元素的score加上增量
 
zrem <key><value>  删除该集合下,指定值的元素
 
zcount <key><min><max>  统计该集合,分数区间内的元素个数
 
zrank <key><value>  返回该值在集合中的排名,从0开始

四、发布订阅

1. 客户端 订阅一个channel (你)

2. 客户端  发布消息  (公众号)

3. 客户端1 接收到消息

发布订阅模式  可以订阅多个频道

五、事务

1. 基本命令

MULTI     # 开启事务

EXEC      # 执行事务;//commit

DISCARD   # 取消事务;//rollback

WATCH     # 监视一些 key,一旦这些 key 在事务执行之前被改变,则取消事务的执行。

2. 有关事务,经常会遇到的是两类错误:

1. 调用 EXEC 之前的错误

        “调用 EXEC 之前的错误”,有可能是由于语法有误导致的,也可能时由于内存不足导致的。只要出现某个命令无法成功写入缓冲队列的情况,redis 都会进行记录,在客户端调用 EXEC 时,redis 会拒绝执行这一事务。

2. 调用 EXEC 之后的错误

        对于“调用 EXEC 之后的错误”,redis 则采取了完全不同的策略,即 redis 不会理睬这些错误,而是继续向下执行事务中的其他命令。这是因为,对于应用层面的错误,并不是 redis 自身需要考虑和处理的问题,所以一个事务中如果某一条命令执行失败,并不会影响接下来的其他命令的执行例子。

3. 悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,

这样别人想拿这个数据就会block直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,

比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

4. 乐观锁

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,

每次去拿数据的时候都认为别人不会修改,所以不会上锁,

但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,

可以使用版本号等机制。乐观锁适用于多读的应用类型,

这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

5. WATCH

        “WATCH” 可以帮我们实现类似于“乐观锁”的效果,即 CAS(check and set)。WATCH 本身的作用是“监视 key 是否被改动过”,而且支持同时监视多个 key,只要还没真正触发事务,WATCH 都会尽职尽责的监视,一旦发现某个 key 被修改了,在执行 EXEC 时就会返回 nil,表示事务无法触发。

代码如下:

        在事务执行之前 如果监听的key的值有变化就不能执行

        在事务执行之前 如果监听的key的值没有变化就能执行

6. 事务回滚:

六、Java 操作 Redis

1. 在java项目的pom文件中添加redis依赖

    <!--jedis-->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>4.3.1</version>
    </dependency>

2. 写一个简单的测试

    @Test
    public void test01(){
        //连接本地的 Redis 服务
        Jedis jedis = new Jedis("192.168.241.33",6379);
        //密码
        jedis.auth("gcf");
        System.out.println("连接成功");
        //查看服务是否运行
        System.out.println("服务器正在运行: "+jedis.ping());

        System.out.println(("--------获取字符串存储的数据并输出-----------"));
        jedis.set("ctb", "才田泊");
        // 获取存储的数据并输出
        System.out.println("redis 存储的字符串为: "+ jedis.get("ctb"));
    }

3. 运行结果

七、开启 Redis 缓存

1. yml配置文件 application.yml

spring:
  redis:
    host: 192.168.241.33
    password: gcf
    database: 1

2. 添加缓存的bean

    /**
     * 缓存处理
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> 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;
    }

3. 配置文件上面加开启缓存的注解

4. 对应需要加缓存的地方加上缓存

        (查部门全部信息)

效果:第一次访问后,会将访问到的数据存储到redis中,再次访问时将从redis中查询,不再访问后台。

八、基于注解的支持

@Cacheable

        @Cacheable可以标记在一个方法上,也可以标记在一个类上。

        当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。

        @Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

        @Cacheable可以指定三个属性,value、key和condition。

参数

解释

example

value

缓存的名称,在 spring 配置文件中定义,必须指定至少一个

例如:

@Cacheable(value=”mycache”)

@Cacheable(value={”cache1”,”cache2”}

key

缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合

@Cacheable(value=”testcache”,key=”#userName”)

condition

缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存

@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

@CachePut

        一般使用在保存,更新方法中。

        在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

@CacheEvict

        @CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除操作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。

1、 allEntries属性

        allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。

@CacheEvict(value="users",key = "#id", allEntries=true)
public void delete(Integer id) {
   System.out.println("delete user by id: " + id);
}
2、 beforeInvocation属性

        清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

@CacheEvict(value="users",key = "#id", beforeInvocation=true)
public void delete(Integer id) {
   System.out.println("delete user by id: " + id);
}

九、在项目中的应用

1. 实体类Dept

@Getter
@Setter
public class Dept implements Serializable {

    private static final long serialVersionUID = 1L;
    @TableId(value = "deptno")
    private Integer deptno;

    private String dname;

    private String loc;
}

2. DeptMapper

public interface DeptMapper extends BaseMapper<Dept> {

}

3. IDeptSerice

public interface IDeptService extends IService<Dept> {

    List<Dept> mylist();

    Dept getByDeptId(Integer id);

    Dept mysaveOrUpdate(Dept dept);

    boolean MyRemoveById(Integer deptno);
}

4. DeptServiceImpl

@Service
// @Cacheable(value = "mytest")    //注解在类上,当前类里面所有方法全部使用缓存
public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements IDeptService {

    @Override
    @Cacheable(value = {"mytest","mytest1"})    //注解在方法上,仅该方法使用缓存
    public List<Dept> mylist() {
        return this.list();
    }

    @Override
    @Cacheable(value = "mytest",key = "#deptno",condition = "#deptno>10")
    // condition 条件  满足条件的时候才会进行缓存;不满足条件不进行缓存
    public Dept getByDeptId(Integer deptno){
        /*QueryWrapper<Dept> wrapper = new QueryWrapper<>();
        wrapper.eq("deptno", deptno);*/
        return this.getById(deptno);
    }

    @Override
    @Cacheable(value = "update",key = "#dept.deptno")
    public Dept mysaveOrUpdate(Dept dept) {
        this.saveOrUpdate(dept);
        return dept;
    }

    @Override
    @CacheEvict(allEntries = true,value = {"mytest","mytest1"}, key = "#deptno")
    public boolean MyRemoveById(Integer deptno) {
        return this.removeById(deptno);
    }

}

5. DeptController

@RestController
@RequestMapping("/dept")
public class DeptController {
    @Resource
    private IDeptService deptService;

    // 查询所有部门信息
    @GetMapping
    public ResponseVo findAll(){
        return ResponseVo.SUCCESS(deptService.mylist());
    }

    // 根据deptno 查询部门的信息
    @GetMapping("{deptno}")
    public ResponseVo getDeptById(@PathVariable Integer deptno){
        return ResponseVo.SUCCESS(deptService.getByDeptId(deptno));
    }

    // 修改/添加
    @PutMapping
    public ResponseVo updateDeptById(Dept dept){
        return ResponseVo.SUCCESS(deptService.mysaveOrUpdate(dept));
    }

    // 删除
    @DeleteMapping("{deptno}")
    public ResponseVo deleteById(@PathVariable Integer deptno){
        return ResponseVo.SUCCESS(deptService.MyRemoveById(deptno));
    }
}

十、验证码

1. 配置文件

        application.properties

# 验证码
sms.host=https://gyytz.market.alicloudapi.com
sms.appcode=675941082cd949768311039080a15928
sms.smsSignId=2e65b1bb3d054466b82f0c9d125465e2
sms.templateId=908e94ccf08b4476ba6c876d13f084ad

2. service层

        IMsgService

public interface IMsgService {
    String sendMsg(String tel);
}

        MsgServiceImpl

@Service
public class MsgServiceImpl implements IMsgService {

    @Value("${sms.host}")
    private String hostT;
    @Value("${sms.appcode}")
    private String appcodeT;
    @Value("${sms.smsSignId}")
    private String smsSignIdT;
    @Value("${sms.templateId}")
    private String templateIdT;

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public String sendMsg(String tel) {
        String host = hostT;
        String path = "/sms/smsSend";
        String method = "POST";
        String appcode = appcodeT;
        Map<String, String> headers = new HashMap<String, String>();
        //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
        headers.put("Authorization", "APPCODE " + appcode);
        Map<String, String> querys = new HashMap<String, String>();
        querys.put("mobile", tel);
        String yzm = String.valueOf((int) (Math.random() * 90000) + 10000);

        querys.put("param", "**code**:" + yzm + ",**minute**:5");

        //smsSignId(短信前缀)和templateId(短信模板),可登录国阳云控制台自助申请。参考文档:http://help.guoyangyun.com/Problem/Qm.html

        querys.put("smsSignId", smsSignIdT);
        querys.put("templateId", templateIdT);
        Map<String, String> bodys = new HashMap<String, String>();

        redisTemplate.opsForValue().set(tel, yzm, 300, TimeUnit.SECONDS);

        try {
            /**
             * 重要提示如下:
             * HttpUtils请从\r\n\t    \t* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java\r\n\t    \t* 下载
             *
             * 相应的依赖请参照
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
             */

            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            /*System.out.println(response.toString());*/
            System.out.println("验证码发送成功!");
            //获取response的body
            //System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return yzm;
    }
}

3. 控制层

        SmsController

@RestController
@RequestMapping("sms")
public class SmsController {
    @Resource
    private IMsgService msgService;

    /**
     * 根据手机号发送验证码
     */
    @GetMapping
    public ResponseVo sendMsg(String tel){
        String val = msgService.sendMsg(tel);
        return ResponseVo.SUCCESS(val);
    }
}

4. 效果

十一、秒杀

        库存中有10件商品 商品的信息自定义 同时有100个人去抢购

1. 在控制层编写秒杀相关的后台代码

@RestController
@RequestMapping("/seckill")
public class MySecKillController {

    private String key="pro:1";

    @PostMapping
    public void begin(String val){
        // reids 存放商品的数量
        Jedis jedis = new Jedis("192.168.241.33",6379);
        jedis.auth("gcf");
        // 存值
        jedis.set(key,val);
    }

    @GetMapping
    public void seckill(){
        // 开始进行秒杀
        Jedis jedis = new Jedis("192.168.241.33",6379);
        jedis.auth("gcf");
        // 秒杀:
        // 1.获取对应的商品数量
        String s = jedis.get(key);
        // 2.判断是否为null
        if(StringUtils.isBlank(s)){
            // null
            System.out.println("活动还未开始!");
        }else {
            // 不为null
            // Watch key
            jedis.watch(key);
            // 值>0
            int i = Integer.parseInt(s);
            if(i>0){
                Transaction multi = jedis.multi();
                // 组装事务
                multi.decr(key);
                // 记录用户id
                multi.sadd("userList", UUID.randomUUID().toString());
                // 减少商品数量
                // Exec
                List<Object> exec = multi.exec();
                if (exec==null||exec.size()==0){
                    System.out.println("活动已经结束!");
                }else {

                    System.out.println("秒杀成功!");
                }
            }else {
                System.out.println("活动已经结束!");
            }
        }
    }
}

2. 创建商品,存放到Redis中

3. 启动 Apache JMeter  创建线程组

4. 设置100个线程来模拟100人

5. 添加HTTP请求

填写请求路径

6. 启动请求

        后台打印

        用户存储到redis中

        商品数量相应减少

十二、Redis持久化之RDB

        Redis 提供了2个不同形式的持久化方式。

        RDB(Redis DataBase)

        AOF(Append Of File)

1.RDB是什么

        在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里

2. 备份是如何执行的

        Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失

3. Fork

Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术

一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

4. RDB持久化流程

5. dump.rdb文件

在redis.conf中配置文件名称,默认为dump.rdb

文件在哪里启动这个文件就在哪里

6. 配置位置

rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下

vim redis.conf

dir /usr/myredis

7. 如何触发RDB快照;保持策略

7.1 配置文件中默认的快照配置时间间隔

7.2 命令save bgsave

save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。

bgsave:Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。

可以通过lastsave 命令获取最后一次成功执行快照的时间

7.3 flushall命令

执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义

7.4 SNAPSHOTTING快照
7.5. Save

格式:save 秒 写操作次数

RDB是整个内存的压缩过的Snapshot,RDB的数据结构,可以配置复合的快照触发条件,

默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。

禁用

不设置save指令,或者给save传入空字符串

7.6. stop-writes-on-bgsave-error

当Redis无法写入磁盘的话(磁盘已满),直接关掉Redis的写操作。推荐yes.

7.7 rdbcompression 压缩文件

        对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会进行压缩。

如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐yes.

7.8. rdbchecksum 检查数据的完整性

在存储快照后,还可以让redis来进行数据校验,如果数据已经损坏就不需要再进行持久化的操作,这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能

推荐yes.

7.9. rdb的备份

先查询rdb文件的目录

将*.rdb的文件拷贝到别的地方

rdb的恢复

关闭Redis

先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb

启动Redis, 备份数据会直接加载

7.10. 优势

适合大规模的数据恢复

对数据完整性和一致性要求不高更适合使用

节省磁盘空间

恢复速度快

7.11 劣势

Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑

虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。

在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

十三、Redis持久化之AOF

1. AOF(Append Only File)

1.1 是什么

        以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

1.2. AOF持久化流程

(1)客户端的请求写命令会被append追加到AOF缓冲区内;

(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

1.3. AOF默认不开启

可以在redis.conf中配置文件名称,默认为 appendonly.aof

AOF文件的保存路径,同RDB的路径一致。

1.4. AOF和RDB同时开启,redis听谁的?

        AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

1.5. AOF启动/修复/恢复

        AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。

正常恢复

修改默认的appendonly no,改为yes

将有数据的aof文件复制一份保存到对应目录

恢复:重启redis然后重新加载

异常恢复

修改默认的appendonly no,改为yes

如遇到AOF文件损坏,通过

/usr/redis/bin/redis-check-aof --fix 文件的位置/appendonly.aof进行恢复

备份被写坏的AOF文件

恢复:重启redis,然后重新加载

1.6. AOF同步频率设置

appendfsync always

始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

appendfsync everysec

每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

appendfsync no

redis不主动进行同步,把同步时机交给操作系统。

1.7. Rewrite压缩
1. 概念

        AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof

2. 重写原理,如何实现重写

        AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。

no-appendfsync-on-rewrite:

        如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

        如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

3. 触发机制,何时重写

        Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发

        重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。

        auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

->40m 80

        auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。

例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB

系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,

如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

4. 重写流程(背)

(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。

(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。

(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。

(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。

(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

2. 优劣分析

2.1 优势

备份机制更稳健,丢失数据概率更低。

可读的日志文本,通过操作AOF稳健,可以处理误操作。

2.2 劣势
  • 比起RDB占用更多的磁盘空间。
  • 恢复备份速度要慢。
  • 每次读写都同步的话,有一定的性能压力。
  • 存在个别Bug,造成恢复不能。
2.3 用哪个好

官方推荐两个都启用。

如果对数据不敏感,可以选单独用RDB。

不建议单独用 AOF,因为可能会出现Bug。

如果只是做纯内存缓存,可以都不用。

2.4 官方建议
  • RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
  • AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.
  • Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大

只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.

同时开启两种持久化方式

  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
  • RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?
  • 建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
  • 性能建议

        因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。

        如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。

        代价,一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。

        只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。

        默认超过原大小100%大小时重写可以改到适当的数值。

十四、主从复制

1. 是什么

        主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主

2. 能干嘛

        读写分离,性能扩展(主 写 从 读)

        容灾快速恢复

3 主从复制

一主二仆

更改端口号

        主:6380

        从:6381 6382

创建目录 /usr/myredis ,在该目录下创建配置文件 redis6380.conf、redis6381.conf、redis6382.conf

include /usr/redis/redis-5.0.14/redis.conf
port 6380
daemonize yes
dir /usr/myredis
dbfilename dump6380.rdb
pidfile /var/run/redis_6380.pid
bind 0.0.0.0
protected-mode no

注意:不能设置密码

        redis6380.conf

        redis6381.conf

        redis6382.conf

启动三台redis服器

查看三台服务器的运行情况

连接客户端 :切换到/usr/redis/bin 目录下执行

 ./redis-cli -p 6380

查看运行 状态:

info replication

        此时三台全部都是主机

配从

6380 主         81 82 从

slaveof <ip><port>

成为某个实例的从服务器

1.在6381和6382上执行: slaveof 自己的ip地址 6380

2.在主机上写,在从机上可以读取数据

3.主机挂掉,重启就行,一切如初

4.从机重启需重设:slaveof ip地址 6380

复制原理

  • Slave启动成功连接到master后会发送一个sync命令
  • Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
  • 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
  • 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
  • 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行

薪火相传

上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。

用 slaveof <ip><port>

        将6382端口的主机改为6381

中途变更转向:会清除之前的数据,重新建立拷贝最新的

风险是一旦某个slave宕机,后面的slave都没法备份

主机挂了,从机还是从机,无法写数据了

反客为主

当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。

用 slaveof no one 将从机变为主机。

        使6380宕机

        让6381成为主机

手动版

哨兵模式(sentinel)

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

调整为一主二仆模式,6379带着6380、6381

自定义的/myredis目录下新建sentinel.conf文件,名字绝不能错

配置哨兵,填写内容

sentinel monitor mymaster 192.168.241.33 6380 1

其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。

启动哨兵

当主机挂掉,从机选举中产生新的主机

(大概10秒左右可以看到哨兵窗口日志,切换了新的主机)

哪个从机会被选举为主机呢?根据优先级别:replica-priority

原主机重启后会变为从机。

领导 (主)

储备干部 员工

优先级在redis.conf中默认:replica-priority 100,值越小优先级越高

偏移量是指获得原主机数据最全的

每个redis实例启动后都会随机生成一个40位的runid

十五、集群搭建

1、应用场景

        当主备复制场景,无法满足主机的单点故障时,需要引入集群配置。

一般数据库要处理的读请求远大于写请求 ,针对这种情况,我们优化数据库可以采用读写分离的策略。我们可以部 署一台主服务器主要用来处理写请求,部署多台从服务器 ,处理读请求。

2、基本原理

        哨兵选举机制,如果有半数节点发现某个异常节点,共同决定改异常节点的状态,如果该节点是主节点,对应的备节点自动顶替为主节点。Sentinel(哨兵)是Redis 的高可用性解决方案:由一个或多个Sentinel 实例 组成的Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。

3、主从复制的作用

1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

4、读写分离:可以用于实现读写分离,主库写、从库读,读写分离不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量。

5、高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础

4、配置集群所需的环境

        Redis集群至少需要3个节点,因为投票容错机制要求超过半数节点认为某个节点挂了该节点才是挂了,所以2个节点无法构成集群。

        要保证集群的高可用,需要每个节点都有从节点,也就是备份节点,所以Redis集群至少需要6台服务器。因为我没有那么多服务器,也启动不了那么多虚拟机,所在这里搭建的是伪分布式集群,即一台服务器虚拟运行6个redis实例,修改端口号为(7001-7006),当然实际生产环境的Redis集群搭建和这里是一样的。

1、规划网络。

        用一台虚拟机模拟6个节点,一台机器6个节点,创建出3 master、3 salve 环境。虚拟机是 CentOS7 ,ip地址192.168.159.34

2、创建 Redis 节点

        首先在 192.168.159.34 机器上 /usr/gcf/目录下创建 redis_cluster 目录;

mkdir redis_cluster

3、创建目录

        在 redis_cluster 目录下,创建名为7001、7002,7003、7004、7005,7006的目录

mkdir 7001 7002 7003 7004 7005 7006

4、在7001目录下配置redis.conf
include /usr/redis/redis-5.0.14/redis.conf
port 7001
pidfile "/var/run/redis_7001.pid"
dbfilename "dump_7001.rdb"
dir "/usr/gcf/redis_cluster/7001"
logfile "/usr/gcf/redis_cluster/7001/redis_err_7001.log"
bind 0.0.0.0
protected-mode no
daemonize yes

cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 15000

        集群配置一般不要设置密码

5、将7001目录下 redis.conf 拷贝到其他这五个目录中

        在 /usr/gcf/redis_cluster目录下运行

echo ./7002 ./7003 ./7004 ./7005 ./7006 | xargs -n 1 cp -v /usr/gcf/redis_cluster/7001/redis.conf

        修改各目录下的 redis.conf 文件

        如:将7002目录下的redis.conf文件中的所有 “7001” 替换成 “7002” 。

6、配置一键启动这6个redis
vim stredis.sh
#!/bin/bash
/usr/redis/bin/redis-server /usr/gcf/redis_cluster/7001/redis.conf
/usr/redis/bin/redis-server /usr/gcf/redis_cluster/7002/redis.conf
/usr/redis/bin/redis-server /usr/gcf/redis_cluster/7003/redis.conf
/usr/redis/bin/redis-server /usr/gcf/redis_cluster/7004/redis.conf
/usr/redis/bin/redis-server /usr/gcf/redis_cluster/7005/redis.conf
/usr/redis/bin/redis-server /usr/gcf/redis_cluster/7006/redis.conf

        启动!

7、创建redis的集群
/usr/redis/bin/redis-cli  --cluster create  192.168.241.33:7001 192.168.241.33:7002 192.168.241.33:7003 192.168.241.33:7004 192.168.241.33:7005 192.168.241.33:7006 --cluster-replicas 1

        cluster-replicas 1

        1其实代表的是一个比例,就是主节点数/从节点数的比例。那么想一想,在创建集群的时候,哪些节点是主节点呢?哪些节点是从节点呢?答案是将按照命令中IP:PORT的顺序,先是3个主节点,然后是3个从节点。

        分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。

配置成功如下图

8、使用cli连接redis集群

使用客户端链接集群 必须使用 -c 连接

/usr/redis/bin/redis-cli -c -p 7001

查看集群的节点的信息 :

cluster nodes

9、检查集群的状态
 /usr/redis/bin/redis-cli --cluster check 192.168.241.33:7003

10、添加主节点

配置文件 7007 /redis.conf

启动

在添加之前首先要像7001-6一样,修改配置文件,启动7007 服务

/usr/redis/bin/redis-cli --cluster add-node 192.168.241.33:7007 192.168.241.33:7003

前面的IP加端口号是要添加的redis节点,后面的IP和端口号是集群中的任意一个节点。

检查节点的状态

        这里可以看到 7007的节点id为 704f6f5de73feabfa9ac7f142edba76557ad3db9

        (按上述操作配置7008)

11、配置从节点

        刚才添加的主节点还没有分配槽,所以无法使用

        添加从节点之前需要设置从节点并启动节点

        --cluster-master-id 为从节点对应主节点的id

配置7008端口号成为7007的从节点

        先将节点接入到集群  设置节点的状态 --cluster-slave --cluster-master-id

/usr/redis/bin/redis-cli --cluster add-node 192.168.241.33:7008 192.168.241.33:7003 --cluster-slave --cluster-master-id 704f6f5de73feabfa9ac7f142edba76557ad3db9

检查节点状态

/usr/redis/bin/redis-cli --cluster check 192.168.241.33:7002

添加的主节点还不能使用,因为没有分配slots,

重新分配槽(slots)

Redis-cli --cluster reshard 集群的节点

 /usr/redis/bin/redis-cli --cluster reshard 192.168.241.33:7001

slot的概念。slot对于Redis集群而言,就是一个存放数据的地方,就是一个槽。对于每一个Master而言,会存在一个slot的范围,而Slave则没有。在Redis集群中,依然是Master可以读、写,而Slave只读

/usr/java/redis/bin/redis-cli --cluster reshard 192.168.159.34:7002

all 代表从所有有数据据节点进行迁移到目的节点,也可以选择 目标节点id回车,最后以done进行结束

done 代表结束

分配成功以后

12、删除节点
12.1 删除从节点:

        从节点用于读数据,没有槽

/usr/redis/bin/redis-cli --cluster del-node 192.168.241.33:7001 ac0c60a3ce8e10729563931ca79d7ccb9504c69f

         尝试删除7007节点

        将7007的槽归还

12.2 删除主节点

        先将槽归还给集群;删除主节点。

        槽 16384

关闭7002

再开启7002

这时就可以按照删除从节点的方法将7002节点删除掉

13、批量添加

集群中进行批量添加

不允许直接使用mset

想添加就需要 分组{名字}

Mset k{u}  当设置了组名的时候分配槽的时候是根据组的名字分配的槽

14. 一键关闭

#!/bin/bash
/usr/redis/bin/redis-cli -h 192.168.241.33 -p 7001 shutdown
/usr/redis/bin/redis-cli -h 192.168.241.33 -p 7002 shutdown
/usr/redis/bin/redis-cli -h 192.168.241.33 -p 7003 shutdown
/usr/redis/bin/redis-cli -h 192.168.241.33 -p 7004 shutdown
/usr/redis/bin/redis-cli -h 192.168.241.33 -p 7005 shutdown
/usr/redis/bin/redis-cli -h 192.168.241.33 -p 7006 shutdown

分配权限

chmod 777 stpredis.sh

关闭集群

./stpredis.sh

启动集群

十六、什么是slots

        [OK] All 16384 slots covered.

        一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个,

        集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。

        name 2 [0-5460]

        集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:

                节点 A 负责处理 0 号至 5460 号插槽。

                节点 B 负责处理 5461 号至 10922 号插槽。

                节点 C 负责处理 10923 号至 16383 号插槽。

1. 在集群中录入值

        在redis-cli每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口。

        redis-cli客户端提供了 –c 参数实现自动重定向。

        如 redis-cli -c –p 7000登入后,再录入、查询键值对可以自动重定向。

        不在一个slot下的键值,是不能使用mget,mset等多键操作。

        可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。(按组分配插槽)

2. Redis 集群优点

        实现扩容

        分摊压力

        无中心配置相对简单

3. Redis 集群的不足

        多键操作是不被支持的

        多键的Redis事务是不被支持的

        由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。

十七、分布式锁

问题描述

        随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁主流的实现方案:

        1. 基于数据库实现分布式锁

        2. 基于缓存(Redis等)

        3. 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点:

        1. 性能:redis最高

        2. 可靠性:zookeeper最高

        这里,我们就基于redis实现分布式锁。

解决方案:使用redis实现分布式锁

redis:命令

# set sku:1:info “OK” NX PX 10000

EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。

PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。

NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。

XX :只在键已经存在时,才对键进行设置操作。

setnx 键不存在的时候 设值

setnx lock test

业务逻辑

del lock

1. 多个客户端同时获取锁(setnx)

2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)

3. 其他客户端等待重试

1.1.1. 编写代码

Redis: set num 0
@GetMapping("testLock")
public void testLock(){
    //1获取锁,setnx
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
    //2获取锁成功、查询num的值
    if(lock){
        Object value = redisTemplate.opsForValue().get("num");
        //2.1判断num为空return
        if(StringUtils.isEmpty(value)){
            return;
        }
        //2.2有值就转成成int
        int num = Integer.parseInt(value+"");
        //2.3把redis的num加1
        redisTemplate.opsForValue().set("num", ++num);
         
        //2.4释放锁,del
        redisTemplate.delete("lock");
    }else{
        //3获取锁失败、每隔0.1秒再获取
        try {
            Thread.sleep(100);
            testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

进行压力测试

问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放

解决:设置过期时间,自动释放锁。

优化之设置锁的过期时间

设置过期时间有两种方式:

1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)

2. 在set时指定过期时间(推荐)

设置过期时间:

代码中设置过期时间

Boolean lock = redisTemplate.opsForValue()

.setIfAbsent("lock", "111",3,TimeUnit.SECONDS);

问题:可能会释放其他服务器的锁。

场景:如果业务逻辑的执行时间是7s。执行流程如下

1. index1业务逻辑没执行完,3秒后锁被自动释放。

2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。

3. index3获取到锁,执行业务逻辑

4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。

最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

优化之UUID防误删

问题:删除操作缺乏原子性。

场景:

1. index1执行删除时,查询到的lock值确实和uuid相等

uuid=v1

set(lock,uuid);

2. index1执行删除前,lock刚好过期时间已到,被redis自动释放

在redis中没有了lock,没有了锁。

3. index2获取了lock

index2线程获取到了cpu的资源,开始执行方法

uuid=v2

set(lock,uuid);

4. index1执行删除,此时会把index2的lock删除

index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行

删除的index2的锁!

LUA脚本保证删除的原子性

KEYS[1] 用来表示在redis 中用作键值的参数占位,主要用來传递在redis 中用作keyz值的参数。

ARGV[1] 用来表示在redis 中用作参数的占位,主要用来传递在redis中用做 value值的参数。

@GetMapping("testLockLua")
public void testLockLua() {
    //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
    String uuid = UUID.randomUUID().toString();
    //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
    String skuId = "25"; // 访问skuId 为25号的商品 
    String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
    // 3 获取锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
    // 第一种: lock 与过期时间中间不写任何的代码。
    // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
    // 如果true
    if (lock) {
        // 执行的业务逻辑开始
        // 获取缓存中的num 数据
        Object value = redisTemplate.opsForValue().get("num");
        // 如果是空直接返回
        if (StringUtils.isEmpty(value)) {
            return;
        }
        // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
        int num = Integer.parseInt(value + "");
        // 使num 每次+1 放入缓存
        redisTemplate.opsForValue().set("num", String.valueOf(++num));
        /*使用lua脚本来锁*/
        // 定义lua 脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 使用redis执行lua执行
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        // 设置一下返回值类型 为Long
        // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
        // 那么返回字符串与0 会有发生错误。
        redisScript.setResultType(Long.class);
        // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
        redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
    } else {
        // 其他线程等待 
        try {
            // 睡眠
            Thread.sleep(1000);
            // 睡醒了之后,调用方法。
            testLockLua();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

keys: k2

val: 2

k2 1

lUA脚本

Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。

Lua 教程_w3cschool

Lua脚本基础入门及其案例_51CTO博客_lua脚本语言入门

LUA脚本在Redis中的优势

将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。

LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。

lua脚本示例

redis.call('set',KEYS[1],ARGV[1])
redis.call('set',KEYS[2],ARGV[2])
local n1 = tonumber(redis.call('get',KEYS[1]))
local n2 = tonumber(redis.call('get',KEYS[2]))
if n1 > n2 then
    return 1
end
if n1 == n2 then
    return 0
end
if n1 < n2 then
    return 2
end
/路径/redis-cli -a 密码 --eval /路径/myluatest.lua k1 k2 , k1值 k2值

注意事项: key  和参数之间要用,隔开  并且,前后两端还有空格

lua脚本示例

local userid=KEYS[1]; 
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid..":usr"; 
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then 
  return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then 
  return 0; 
else 
  redis.call("decr",qtkey);
  redis.call("sadd",usersKey,userid);
end
return 1;

出错

针对如上错误,作如下处理:

1)查看打开文件的上限和redis服务进程,修改上限

输入如下命令,查看其上限:

ulimit -a

设置上限

ulimit -n 10032

重启redis即可

redisson

        Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。简单说就是redis在分布式系统上工具的集合,Redission提供了分布式锁的多种多样的功能.

使用redissoncheck

        自定义redis分布式锁无法自动续期,比如,一个锁设置了1分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,可能会导致严重的线上问题,在秒杀场景下,很容易因为这个缺陷导致的超卖了。

        10 2s

1s        

        redission 超时时间1m 执行逻辑的时候3m

锁的分类:

1 、乐观锁与悲观锁

        乐观锁

        悲观锁

2 、可重⼊锁和⾮可重⼊锁

        可重⼊锁:当在⼀个线程中第⼀次成功获取锁之后,在此线程中就可以再次获取

        ⾮可重⼊锁

3 、公平锁和⾮公平锁

        公平锁:按照线程的先后顺序获取锁

        ⾮公平锁:多个线程随机获取锁

4 、阻塞锁和⾮阻塞锁

        阻塞锁:不断尝试获取锁,直到获取到锁为⽌

        ⾮阻塞锁:如果获取不到锁就放弃,但可以⽀持在⼀定时间段内的重试

—— 在⼀段时间内如果没有获取到锁就放弃

Redission的使⽤

1 、获取锁 —— 公平锁和⾮公平锁

    // 获取公平锁
    RLock lock = redissonClient . getFairLock ( skuId );
    // 获取⾮公平锁
    RLock lock = redissonClient . getLock ( skuId );

2 、加锁 —— 阻塞锁和⾮阻塞锁

    // 阻塞锁(如果加锁成功之后,超时时间为 30s ;加锁成功开启看⻔狗,剩 5s 延⻓过期时间)
    lock . lock ();
    // 阻塞锁(如果加锁成功之后,设置⾃定义 20s 的超时时间)
    lock . lock ( 20 , TimeUnit . SECONDS );
    // ⾮阻塞锁(设置等待时间为 3s ;如果加锁成功默认超时间为 30s )
    boolean b = lock . tryLock ( 3 , TimeUnit . SECONDS );
    // ⾮阻塞锁(设置等待时间为 3s ;如果加锁成功设置⾃定义超时间为 20s )
    boolean b = lock . tryLock ( 3 , 20 , TimeUnit . SECONDS );

3 、释放锁

 lock . unlock ();

4 、应⽤示例

    // 公平⾮阻塞锁
    RLock lock = redissonClient . getFairLock ( skuId );
    boolean b = lock . tryLock ( 3 , 20 , TimeUnit . SECONDS );

Redisson 锁加锁流程:线程去获取锁,获取成功则执行lua脚本,保存数据到redis数据库。如果获取失败: 一直通过while循环尝试获取锁(可自定义等待时间,超时后返回失败)。Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。

 @Autowired
   private RedissonClient redissonClient;

    public void method1() {
        RLock lock = redissonClient.getLock("lock");
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败,1");
        }
        try {
            log.info("获取锁成功,1");
            method2();
        }finally {
            log.info("释放锁,1");
            lock.unlock();
        }
        
    }
    public void method2() {
        RLock lock = redissonClient.getLock("lock");
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败,2");
        }
        try {
            log.info("获取锁成功,2");
        }finally {
            log.info("释放锁,2");
            lock.unlock();
        }
    }

使用redission

加jar

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--配置redission-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.12.5</version>
        </dependency>

application文件


# 设置redis的信息
spring.redis.host=192.168.241.33
spring.redis.database=1
spring.redis.password=gcf
spring.redis.port=6379

配置类:


package com.example.bootdemo.aaa.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String redisPassword;

    @Bean
    public RedissonClient getRedisson(){

        Config config = new Config();
// //多节点config.useClusterServers()
        //单机模式  依次设置redis地址和密码
        config.useSingleServer().
                setAddress("redis://" + host + ":" + port).
                setPassword(redisPassword);
        return Redisson.create(config);
    }
}

controller

package com.example.bootdemo.aaa.controller;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Objects;

@RestController
@RequestMapping("/redisLock")
public class RedisLockController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redisson;

    private static final String REDIS_KEY = "redis_test";

    private static final int MAX_SIZE = 10;

    /**
     * 初始化库存
     */
    @PostMapping("/init")
    public void init() {
        stringRedisTemplate.opsForValue().set(REDIS_KEY, String.valueOf(MAX_SIZE));
    }

    /**
     * 扣库存业务
     */
    @PostMapping("/test")
    public void exportInventory() {

        String lockKey = "product001";
        RLock lock = redisson.getLock(lockKey);
        try {
            lock.lock();
            int s = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(REDIS_KEY)));
            System.out.printf("1号服务:库存当前为:" + s + "\n");
            //stringRedisTemplate.opsForValue().set(REDIS_KEY, String.valueOf(s));
            if(s>0) {
                stringRedisTemplate.opsForValue().decrement(REDIS_KEY);
            }
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }
}

使用jmeter测试:

写两个一模一样的项目 通过jmeter访问 redisLock/test

两个服务中的数据不重复即可

Redisson分布式锁入门使用(可重入锁(lock))_spring boot_人生没有第三次-华为云开发者联盟

使用集群

RedisonConfig.java

package com.aaa.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.ClusterServersConfig;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedisonConfig {

    //@Value("${spring.redis.host}")
    //private String host;

    //@Value("${spring.redis.port}")
    //private String port;

    //@Value("${spring.redis.password}")
    //private String redisPassword;

    @Bean
    public RedissonClient getRedisson(){

//        // config
        Config config = new Config();
        //多节点config.useClusterServers()

        ClusterServersConfig clusterConfig = config.useClusterServers()
                .addNodeAddress("redis://192.168.241.33:7001")
                .addNodeAddress("redis://192.168.241.33:7002")
                .addNodeAddress("redis://192.168.241.33:7003")
                .addNodeAddress("redis://192.168.241.33:7004")
                .addNodeAddress("redis://192.168.241.33:7005")
                .addNodeAddress("redis://192.168.241.33:7006");

        //单机模式  依次设置redis地址和密码
//        config.useClusterServers()
//                setAddress("redis://" + host + ":6379").
//                setPassword(redisPassword);
//        return Redisson.create(config);

        return Redisson.create(config);
    }
}

RedissonSeckillController.java

package com.aaa.controller;

import org.omg.CORBA.PUBLIC_MEMBER;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.TimeUnit;


@RestController
@RequestMapping("redseckill")
public class RedissonSeckillController {

    @Resource
    private RedisTemplate redisTemplate;


    @Resource
    private RedissonClient redissonClient;

    @GetMapping("init")
    public void init(){
        redisTemplate.opsForValue().set("proid",10);
    }

    @GetMapping
    public void ms(){
        // 加锁
        RLock lockpro = redissonClient.getLock("lockpro");
        try {
            lockpro.lock(2000, TimeUnit.MILLISECONDS);// 看门狗
            // 开始处理业务
            // 1. 商品的库存
            String proid = String.valueOf(redisTemplate.opsForValue().get("proid"));
            int i = Integer.parseInt(proid);
            if (i > 0) {
                // 可以秒杀
                redisTemplate.opsForValue().decrement("proid");
                //
                redisTemplate.opsForSet().add("userList", UUID.randomUUID());
            } else {
                System.out.println("秒杀结束");
            }

        }catch (Exception e){
            System.out.println(" 出现异常");
        }finally {
            lockpro.unlock();// 释放掉锁
        }
    }
}

运行程序

        设置10个商品


        注意:连接集群要勾选 Cluster 

       用JMeter 对   发送一百次请求

        查看结果:正常情况下后台应打印90行 “ 秒杀结束 ”

        proid 中数据变为 0

        usrList中存了10个用户

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值