文章目录

  • 简介
  • 安装CentOS7
  • 安装与使用
  • Key操作
  • String类型
  • List 集合
  • Set 集合
  • Hash 类型
  • Zset 集合
  • 配置文件
  • 发布和订阅
  • BitMap
  • HyperLogLog
  • Geospatial
  • Jedis 操作Redis
  • SpringBoot整合redis
  • 事务
  • 事务的基本操作和错误处理
  • 事务冲突的问题
  • 悲观锁
  • 乐观锁。
  • 特性
  • 秒杀案例1.0
  • 秒杀案例2.0
  • 秒杀案例3.0
  • 秒杀案例4.0
  • RDB之持久化技术
  • AOF之持久化技术
  • 主从复制
  • 简介
  • 搭建
  • 一主两从特点
  • 原理
  • 薪火相传
  • 反客为主
  • 哨兵模式
  • 规则
  • Jedis集成主从复制哨兵模式
  • 缺点
  • 集群
  • 简介
  • 搭建
  • 故障修复
  • slots
  • Jedis连接集群
  • 缺点
  • 缓存穿透
  • 缓存击穿
  • 雪崩
  • 分布式锁
  • 简介
  • 问题



简介

  • Redis是一种典型的单线程+多路IO复用的NoSQL数据库。
  • NoSQl数据库为了解决分布式存储的问题。
  • 比如多个请求分步在不同的服务器进行访问,但只能产生一个session,按道理说这个session只能存在一个服务器,这就造成其他服务器找不到已经验证过的session。解决这个问题,有很多种,比如,我们可以将其存在cookie中,但这个Cookie是保存在客户端的,所以不太安全,还有我们可以将第一台保存session的服务器中session复制到其他服务器中,这样就让所有的服务器都保持同一个session,但是这样会造成空间的极度浪费。所以就有了NoSQL数据库的出现,其提供了一种缓存机制,当我们创建了一个session后,保存在缓存中,下一次只要改缓存数据库中验证就可以了。
  • NoSQL数据库还有一种很好的作用,缓解IO压力,当我们的数据保存在数据库的时候,随着时间的日积月累,数据量也会增加,当我们每次进行增删改查的操作时,难免会出现IO请求的缓慢。同样的,我们也可以将常用的一些增删改查数据放在缓存数据库中以增加速度。
  • NoSQL数据库不同于mySql数据库,其是一种非关系型数据库,使用key-value简单的键值对进行存储,将数据持久化存储,支持事务操作。
  • 在现实中,redis 使用最典型的是海量的用户进行秒杀商品的功能。

安装CentOS7

  • 下载镜像文件CentOS-7-x86_64-DVD-2009.iso。
  • 下载VM,打开,新建虚拟机,选择自定义,镜像文件,NAT桥接模式,Linux系统CentOS7,选择总空间一般20g,运行内存大小2g,CPU处理器按自己的电脑配置来定,配置好后,会自动打开虚拟机,若没有自动打开,查看配置,全配置完,再打开。
  • 打开后,选择语言,然后选择上海分区,最后为磁盘分配空间,先选择我要分配,然后选择第二个,最后点击完成。
  • 出现+ 页面,点击+,选择/boot 分配500m,选择/home 分配1000m,选择/swap 分配2000m,最后选择/ 自动分配剩余的。
  • 点击完成,跳回页面,点击安装,安装过程配置root密码。
  • 安装完成,点击重启,进入页面,最小安装的没有图形页面。会提示登录,账号会有显示,密码没有显示。
  • 登录成功后,输入cd /etc/sysconfig/network-scripts 然后输入ls,查看第一个文件名。最后输入vi 文件名。
  • 进入该文件后,点击键盘字母i进入修改模式,最后修改成以下图片形式
  • 嵌入式linux 编译 redis_嵌入式linux 编译 redis

  • 修改成功后,输入esc退出编辑模式,然后输入service network restart 重启网络。
  • 重启成功后,输入ping www.baidu.com 看是否出现ping通,ping通标志为不断输出字母,没有停止。想要停止按住ctrl+c。
  • ping通后表示网络没有问题,最后下载XSheel可以连接远程库,然后输入ssh + ip地址,这个ip地址为IPADDR的配置,如果配置不同的系统,这个IPADDR应该不一样,其他一样,最后.99 需要改变。如果输入ssh+ip ,会弹出输入系统账号和密码,最后如果能连接成功表明没有问题。

安装与使用

  • 下载redis 最新版本,安装linux 操作系统CentOS7以上。
  • 安装Xshell 进行远程操控给linux系统,安装Xftp 进行linux与windows的文件传输。
  • 将文件传到linux操作系统的opt目录下,然后打开客户端,输入命令进行opt目录下,在安装redis之前,要有一个c语言的编译环境gcc。
  • OS 8 以上版本默认安装,要确定自己是否安装了 gcc 可以输入命令
gcc --version

若是没有任何信息,可输入
安装gcc的编译环境
安装成功后,可再次输入 gcc --version命令看是否安装成功。

  • 安装gcc后,可输入以下命令对redis 压缩包进行解压
tar -zxvf redis文件名
  • 解压后,输入命令进入该解压包。
  • 然后输入以下命令进行c的编译
make

如果安装过程报错,可输入以下命令清楚之前的记录

  • 编译成功后,可以对其进行安装,输入命令
make install
  • 安装成功后,文件会保存在usr/local/bin目录下,可进行该文件夹输入
ll

进行查看,有redis多个字样就表明安装成功

  • 启动服务器
  • 前台启动,不推荐,将客户端窗口关闭后会失效,输入redis-server 可进行前台启动。停止前台启动的快捷键为ctrl+c。
  • 后台启动
  • 进入opt目录中的redis目录,找到redis.conf 文件输入命令cp redis.conf /etc/redis.conf 复制到etc目录中,然后再进入etc目录。可查看是否有该文件。
    在etc目录下输入 vi redis.conf 进入文件内容,然后输入 /daemonize 查询到该字段,要是值为no的话要改为yes ,点击键盘字母i进行输入模式,修改为yes 后。按 esc 推出输入模式,然后输入 :wq 退出编辑。
  • 编辑完成后,输入cd /usr/local/bin 进入该目录,然后 输入命令 redis-server /etc/redis.conf 启动服务器。
  • 启动完成后,可输入命令 ps -ef | grep redis 查看该服务器进程,可见其端口号为6379。
  • 后端启动的好处是,即使客户端窗体关闭依旧能运行,同时我们可以在bin目录下用 redis-cli 命令将客户端与服务端进行连接。出现ip和端口号表明连接成功。测试是否为联通状态还可以输入ping 命令可以看见输出了pong字样就表示已联通。
  • 关闭服务可通过shutdown进行关闭

Key操作

  • set key value 命令可以设置一对键值对。
  • keys * 表示查看所有的键值对,注意中间空格和key是复数。
  • exists key 判断某个key是否存在。存在返回,不存在返回0
  • type key 查看key的类型
  • del key 和unlink key都可以删除指定key,效果虽然一样,但是前者为直接删除,后者要等会删除。
  • expire key date 设置已存在key指定多少时间后过期,秒为单位。
  • ttl key 查看当前key 时间,是否过期,-2表示过期,-1表示永不过期。
  • select index 表示切换到哪个数据库。redis 中只含有16个库,从0开始,初始也为0.
  • dbsize 表示当前数据库有多少key。
  • flushdb 清空当前库。
  • flushall 清空所有库。

String类型

  • String类型是redis最基本的数据类型,其是二进制安全的,通常用于存储字符和图片。一个redis字符串中的value值,最大只能是512m
  • 基本命令
  • set key value 设置键值对,当键存在时会覆盖原来的value。
  • setnx key value。设置键值对,当键一定不在村时候。
  • get key 获取某个key的value
  • append key向某个key 的value后追加字符串。返回追加后的长度。
  • strlen key 获取key的value的长度。
  • incr key 将key的数值value增加1.
  • decr key 将key的数值value减1.
  • incrby key dept 将key的value值按照某个步长增长。
  • decrby key dept 将可以的value的值按照某个步长减少。
  • mset key value key value 设置多个key-value
  • mget key key 获取多个key的value
  • msetnx key value key value 设置多个原来不存在的值。
  • getrange key index end 获取某个key的value值,后者的index 和end表示取value中的某个范围的字符串。
  • setrange key index value在某个key的value值中的某个位置设置改变某个值。
  • setex key date value 设置某个值,并规定过期时间,秒为单位
  • getset key value 返回旧值,但value会覆盖原来的值。
  • 在设置值的时候,我们可以看到,在redis中,其最基本的设置值的时候是原子性操作的,因为其单线程的特性,大家都互相不影响。而在java中,一个值的设置比如进行++i的操作,加入有多个线程运行,这些线程是会互相影响的。

List 集合

  • 集合类型可以放多个值,其底层是一个双向链表结构,可以从左到右的遍历,反之亦可。
  • 其基本的命令有
  • lpush / rpush key v1 v2 v3 …表示从左边/右边存储数据。
  • lpop / rpop key 从key 列表的左边/右边吐出一个值。
  • rpoplpush k1 k2 从k1 右边吐出一个值插入到k2左边
  • lrange key index end 从固定范围取值,要取全部,一般用lrange key 0 -1 0表示第一个,-1表示最后一个。
  • lindex key 根据指定下标取值。
  • llen key 获取列表的长度。
  • linsert key before/after value newValue 在key列表的value前/后插入一个newValue
  • lrem key count value 从左到右删除key列表中相同的count个value。
  • lset key index value 将key列表下标为index的值设为value。
  • 底层:一个快速链表结构,当元素比较少时,这些元素会压缩成一个块,称为压缩链表。当元素较多时,会出现不同的压缩链表,这些压缩链表就会以链表的形式构建起来,形成快速链表。

Set 集合

  • set集合如List集合一样,都是一个集合,但set集合主要的区别在于其内部自动排重,且底层使用hashMap ,其时间复杂度为O(1),能快速寻找值。
  • 常用命令有
  • sadd key v1 v2 v3…设置key集合的值
  • smembers key 取出该集合的所有值
  • sismember key value 判断是否存在某个值,存在1,不存在0
  • scard 返回集合的元素个数。
  • spop 随机从集合吐出一个值。
  • srandmember key n 随机从集合取出n个值,不会改变集合结构。
  • srem key v1 v2 v3 删除集合某些元素
  • smove k1 k2 value 将k1集合的value值转移到k2集合中。
  • sinter k1 k2 取集合的交集。
  • sunion k1 k2 取集合的并集
  • sdiff k1 k2 取集合的差集。

Hash 类型

  • 存储key-value键值对,类似java的Map ,特别适合存储对象。
  • 若是存储对象,key 为对象名称,而value 也要变为键值对结构,可以成为filed-value
  • 常用的命令有:
  • hset key filed value 添加一个名为key的Map结构。其值有一个filed-value
  • hget key filed 获取某个key的对应的某个的filed的值。
  • hmset key f1 v1 f2 v2 一次性为key结构设置多个值。
  • hexists key filed 判断某个key结构是否存在某个filed
  • hkeys key 列出该hash集合里的所有filed
  • hvals key 列出key结构中的所有值。
  • hincrby key filed upValue 向某个key结构的某个filed的值加上upValue
  • hsetnx key filed value 给某个key结构的不存在的filed设置值 。

Zset 集合

  • 和普通集合非常相似,但是这是一个有序且没有重复的集合。其底层的不同之处在于每个元素都关联了一个评分机制,用于集合中排序。集合的元素是唯一的,但是评分却是可以重复。
  • 常用的命令有
  • zadd key c1 v1 c2 v2添加一个key,c为评分标准,v为设计了c评分的值。
  • zrange key index end 取出key集合指定范围的元素。从index开始,end结束,一般0 到-1表示全部元素。默认评分从小到大排序。若要将评分显示 zrange key index end with scores 。
  • zrangebyscore key s1 s2 [withscores] 取出某个评分(从小到大)范围的值。
  • zrevrangebyscore key s2 s1 [withscores] 取出某个评分(从大到小排序)范围的值。
  • zincrby key increValue value 给key 集合的某个值为value的score 增加increValue。
  • zrem key value 删除key集合的某个值。
  • zcount key index end 计算评分在某个的范围的值的个数。
  • zrank key value 查找key集合的值为value的评分排名。从0开始。

配置文件

  • 进入redis.conf 修改一个地方 127.0.0.1 ::-1 将#加上(注释)。
  • 同时修改protected-mode yes 将yes改为no。

发布和订阅

  • 发布和订阅是一种消息模式,发布者可以发布多个频道,订阅者只能获取自己订阅的频道信息。
  • 可以创建两个客户端进行演示,第一个客户端我们用来发送消息,指令为publis channel1 hello,
    第二个客户我们用来订阅该频道,指令为SUBSCRIBE channel1。如果我们先订阅该频道,一旦第一个客户端发送了消息,第二个客户端就立马会接收到该消息。

BitMap

  • redis6一种新数据类型,其底层是一个数据,用来操作偏移量。比如统计某个用户是否访问过某个网站,可以将其放在BitMap中。

HyperLogLog

  • 在开发中,我们经常遇到的问题是,要计算网站的访问量,这种功能要通过Redis中incr incrby便可以做到,但像UV访问量计算,要做到去重功能,这些便难以实现。
  • 在mysql中,有distinct 关键字进行去重,在redis中,有Set,Hash,bitMap来解决这些问题,但对于超级大的访问量,这些方法依旧会占用很大的资源。
  • 而HyperLogLog便是支持更大的以解决这类的问题的数据类型,在redis6新增。该数据类型利用基数计算的算法实现,仅需要12kb便可以统计2的十六次方个不同元素的基数,消耗内存非常小。
  • 基数问题:集合去重的问题,称为基数问题,一个集合的基数表示该集合中不重复的元素个数。
  • 基本指令有
  • pfadd key v1,v2,v3… 向key集合中添加元素。由于该类型是为了解决基数类型,所以redis规定,再向该类型的集合添加数据的时候,不能添加重复数据。虽然和前面的集合很像,但其底层是完成不一样的。
  • pfcount key 计算key集合的元素个数。(基数)
  • pfmerge key k1 k2 将k1和k2 合并成key集合

Geospatial

  • 地理类型。早在redis3.2中便提供了设置地理位置的类型,redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
  • 常见命令:
  • geoadd key x y city给city地方添加经纬度,x表示经度,y表示纬,命名为key。
  • geopos key city 返回key中city的经纬度。
  • geodist key c1 c2 fmt 取两个地方的距离,fmt为单位m/km/mi(英里)/ft(英尺)。其经纬度查阅。
  • georedius key x y redius fmt 取出以fmt为单位的redius 半径 x y 经纬度为中心的地方。

Jedis 操作Redis

  • jdbc可以操作数据库。那便可以猜出Jedis也是一个操作数据库的java语言。
  • 要用Jedis操作redis的首要条件,打开linux系统,然后用远程打开redis服务器,只有这样才能连接。
  • 其次,关闭linux的防火墙,依照配置文件那一章进行操作。
  • 然后打开idea ,新建一个Maven工程,导入两个依赖
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.6.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.25</version>
    <scope>compile</scope>
</dependency>
  • 下面是基本的连接操作
public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.188.99", 6379);
        System.out.println(jedis.ping());
    	jedis.close();
}

出现pong字样表示连接成功,之后便可以像用远程操控一样操作redis

  • 例子:验证码的发送
  • 每个手机每天只能发送3次。每个验证码过期时间120s;
package com.hyb;

import redis.clients.jedis.Jedis;

import java.util.Random;

public class JedisCode {
    public static void main(String[] args) {
        setTime("18277486571");
    }

//    获取验证码
    public static String getCode(){
        StringBuilder s=new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < 4; i++) {
            int k = random.nextInt(10);
            s.append(k);
        }
        return s.toString();
    }

//    设置过期时间
    public static void setTime(String phone){
        Jedis jedis = new Jedis("192.168.188.99", 6379);
        String pCount = jedis.get(phone+"_count");
        if (pCount==null) {
            jedis.setex(phone+"_count",24*60*60,"1");
        }else if (Integer.parseInt(pCount)<=2){
            jedis.incr(phone+"_count");
        }else {
            System.out.println("今天发送验证码已超过三次");
            jedis.close();
            return;
        }
        String code = getCode();
        System.out.println(code);
        jedis.setex(phone+"_code",120,code);
        jedis.close();
    }
}

SpringBoot整合redis

  1. SpringBoot整合redis很简单,先导入一个依赖
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  1. 然后写一个配置类:
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        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);
        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<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)) //600秒过时

                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))

                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config) .build();
        return cacheManager;
    }
}
  1. 之后就可以操作了,怎么操作呢?直接注入一个rediesTemplate
@Component
public class RedisUtils{

    @Autowired
    RedisTemplate<String,String> template;


    public  void setHash(String key,String key1,String value1,long timeout,TimeUnit timeUnit){
        //记录登录次数
        template.opsForHash().put(key,key1,value1);
//        System.out.println(redisTemplate.opsForHash().size("loginCount"));
        template.expire(key,timeout, timeUnit);
    }

    public  void set(String key,String value,long timeout,TimeUnit timeUnit){
        template.opsForValue().set(key,value,timeout,timeUnit);
    }

    public  String get(String key){
        return template.opsForValue().get(key);
    }

    public Set<String> getSet(String key){
        return template.opsForSet().members(key);
    }



    public long countHash(String hashKey){
        return template.opsForHash().size(hashKey);
    }

    public boolean delHash(String hashKey){
        return Boolean.TRUE.equals(template.delete(hashKey));
    }

}
  1. 所以,在开发中,我们可以用redis做什么?

最重要的是缓存,比如下面有一个场景: 假如有一个步骤条提交表单的操作,每一个步骤条都对应一个数据表的插入,但是从业务上讲你这个操作要等到步骤条结束后才能进行统一提交,如果有一个提交失败了,或者有用户不想提交,全部得提交失败.
如果是传统的做法,每进行一个步骤的表单提交就插入一次数据库,如果有一个操作失败了,就去对应的数据库将所有操作成功的数据库删除.但是这样子太频繁了,如果有人恶意去做这些操作,对数据库的压力是很大的,所以这个时候就需要用到redis,可以将redis将每个步骤的数据线先缓存起来,最后去redis拿出来,将数据进行插入,如果不成功,直接删除redis即可,这个redis因为是内存操作,所以数据操作是非常快的.

那么还有一种redis更高级的用法,用于微服务模式下的校验.我们都知道在微服务下的路径都是要经过路由去转发,这个时候就可以做一个认证中心,该认证中心独立出一个微服务,让需要被验证的路径在网关处判断,然如果没有权限,就提醒去认证中心去认证权限,如果有权限就放行.
在这个过程中,一般都是使用前端传过来的token进行一个用户权限认证,但是我们不可能将权限都放在token里,这样子前端是可以修改的,所以只能解析这个token,然后查到对应的用户,根据该用户去查询对应的权限,所以这些权限的操作必须得放在数据库里,这个数据库必须是mysql+redis结合完成才能保证数据的完整性和高并发的场景,前者是第一次用户注册的时候保存的持久化的权限列表,后者是在登录之后保存短暂的权限列表,这样子用户在规定时间内多次登录,就不用多次去查询数据库.

redis的操作远不止这么多,其实最主要的是我们得理解redis是独立出来的,并且呢redis将数据暂存在内存里,减少了传统的mysql数据库的刷盘操作,所以在性能上往往比mysql更高,特别适合存储在高并发场景下产生的中间数据.

事务

事务的基本操作和错误处理

  • redis 中的事务同mysql中的事务类似,但不是完全相同的,redis中的事务是指执行一系列命令的操作,在事务这条操作线上,是不允许其他命令进行插队的。
  • 在redis中实现一个事务有基本的三个关键字。multi 表示开始组队,exec 表示执行事务,discard表示在事务过程中进行停止。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> discard
OK
  • 在redis中,如果组队的时候任何一个命令出错,后面都不执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3
(error) ERR wrong number of arguments for 'set' command
  • 如果执行中任何一个命令出错了,其他的命令都能执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> incr k1 
QUEUED
127.0.0.1:6379(TX)> exec 
1) OK
2) OK
3) (error) ERR value is not an integer or out of range

上面的例子可以看出,组队中有一个命令应该是不可以执行的,但是此命令本身没有错,所以能组队,而执行错误是因为其命令对k1是不生效的。但是不影响其他命令的执行

事务冲突的问题

  • 当多个人共享一个账户,钱财本身的总量不能达到每个人想支出的费用的总和,又因为这三个人每个都是一个独立的事务,就会造成事务冲突。

悲观锁

  • 为了解决这个问题,提出了悲观锁,顾名思义,这个方法是“悲观“的,它的原理是,一个账户每次只能由一个人进行访问,每次访问的时候一定得上锁,他人只能在这个锁解开进行访问。
  • 但这个方案是单线程的,虽然安全,但是效率是非常低的。

乐观锁。

  • 其基本思想是,不给账户上锁,但是给账户记录版本号,加入有多个人进行访问,谁先访问就更换账户版本。而每个人要进行访问之前就必须进行版本的检查。
  • 乐观锁常见于抢票当中,一万人抢一张票,所有人都可以抢到,但是只有一个人能支付成功。
  • 实现了乐观问题,我们可以在进行事务之前,对其进行watch操作。
127.0.0.1:6379> watch k
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby k 1
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 6

上面是一个客户端,下面我们再开另一个客户端也对其进行watch操作
可以看到,一旦对事务进行watch操作,可以进行访问,但只能被提交一次。
与watch相反的,unwatch代表取消监视,但是一旦watch整个流程完成后,就不需要unwatch了。所以上面的例子要先实现效果,一定要再其他过程没有完毕时watch。

特性

  • 单独的隔离操作:事务中每一个命令都不互相影响,事务在进行的过程中,不会被其他客户端发送过来的命令进行打断。
  • 没有隔离级别的概念。队列中的命令在没有提交之前不会被执行。
  • 不保证原子性,任何一条命令失败,其后面的命令都会执行,没有回滚的操作。

秒杀案例1.0

  • 在这里我们可以做一个秒杀案例,要求页面中有一个按钮,用来秒杀苹果13,点击一次进行秒杀一次。
  • 页面问题容易实现,主要问题在于秒杀过程中,商品的存储问题。
  • 在秒杀过程中,我们可以传入 用户id和商品id保证信息的唯一性。因为一个用户只能秒杀一个商品。
  • 其次,便是秒杀过程中的库存问题。
  • 最后,秒杀成功后,将数据的变化保存在redis中。
  • 其主要实现如下。
public static boolean doSecKill(String uid,String prodId) throws IOException {
//		uid和prodid的操作

		if (uid==null||prodId==null){
			return false;
		}

//		连接redis

		Jedis jedis = new Jedis("192.168.188.99", 6379);
	

//		拼接key
//			库存key  用户key
//		库存
		String kc="stockProdId:"+prodId;
//		user
		String user="userProdId:"+uid;

//		判断库存是否为空,如果为null,秒杀未开始

		String kcs = jedis.get(kc);
		if (kcs==null){
			System.out.println("秒杀还未开始");
			jedis.close();
			return false;
		}

//		判断用户是否秒杀

		/*
		* 谁秒杀成功,就取其值,因为其值存在set集合中,而且其特性是没有重复数据
		* 所以秒杀不成功,取到的值一定是null的
		* */
		if (jedis.sismember(user,uid)){
			System.out.println("抱歉,你已经秒杀过!");
			jedis.close();
			return false;
		}

//		如果库存数量为0,秒杀结束

		if (Integer.parseInt(kcs)<=0){
			System.out.println("商品已全部被秒杀");
			jedis.close();
			return false;
		}


//		秒杀过程
//			秒杀成功,库存减少,加入秒杀清单中

		jedis.decr(kc);

		jedis.sadd(user,uid);

		System.out.println("秒杀成功了");

		jedis.close();

		return true;
	}
  • 写好代码后,我们启动redis服务器。然后设置商品库存。便可以进行测试
127.0.0.1:6379> set stockProdId:0101 10

秒杀案例2.0

  • 但在前面的1.0版本中,商品秒杀只是个人操作,实际中会产生多人同时操作,也就是并发效果。
  • 但在这个并发的过程中,必须只能有一个结果,即只能有一个人在某刻秒杀成功一个商品。
  • 要进行并发过程,需要安装一个模拟并发的工具ab,在CentOS6中默认安装,但CentOS7需要安装,执行命令 yum install httpd-tools即可。
  • 安装完毕后,可输入ab --help 可查看工具说明,在里面,有几个重要的命令
  • -n requests Number of requests to perform 表示请求数量
  • -c concurrency Number of multiple requests to make at a time 表示请求中并发数量
  • -p postfile File containing data to POST. Remember also to set -T 提交参数时 针对POST请求
  • -T content-type Content-type header to use for POST/PUT data, eg 提交参数的类型
  • 之后,我们便可以根据上面的规则,进行一条并发指令的编写。
ab -n 1000 -c 100 -p ~/profile -T application/x-www-form-urlencoded http://192.168.3.148:8081/Seckill/doseckill

在这条命令中,最后要附上一个地址,该地址表示你要进行测试的工程地址,因为该工具无法识别本机,所以要将自己的ip地址代替localhost,同时需要写上工程的端口号,后面便是Serverlet的访问路径。
其次,实现-p指令,要提前建立一个新的文件,比如另开一个客户端,直接输入命令vi profile 便可以生成一个新文件,然后点击键盘 i 编辑,输入字符串 prodid=0101& 该字符串表示我们写死的商品id,其次是后面& 如同json一样,表示连接符。写完后 点击esc退出输入模式,然后输入:wq 指令保存文件并退出。

  • 退出后,新建一个客户端,启动redis服务器,然后为商品设置库存,之后再第一个客户端里,输入我们编写好的并发指令,点击enter启动该,之后我们便发现我们的idea控制台便不断输入是否秒杀成功的字样。
  • 秒杀完毕后,打开客户端,查看库存,会发现,库存可能会出现负数,这是高并发带来的问题。因为在某一个时刻,总会有两个用户同时挤进去秒杀,服务器又处理不过来,就会造成负数。这种问题被称为超卖的问题。但不仅仅是超卖,要是中间出现网络问题,也会产生问题,这也叫超时问题。

秒杀案例3.0

  • 在redis中,我们只需要将对redis连接该为对redis连接池连接便可以解决超时问题。
  • 首先,需要一个配置类
package com.atguigu;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

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, "192.168.188.99", 6379, 60000 );
            }
         }
      }
      return jedisPool;
   }

   public static void release(JedisPool jedisPool, Jedis jedis) {
      if (null != jedis) {
         jedisPool.returnResource(jedis);
      }
   }

}
  • 之后,我们要将原来秒杀过程中,连接jedis的部分该为连接池连接。
Jedis jedis = new Jedis("192.168.188.99", 6379);

变为

  • 连接池配置完毕后,我们要解决超卖问题,在前面我们学过事务,所以可以通过事务进行解决
public static boolean doSecKill(String uid,String prodId) throws IOException {
//    uid和prodid的操作

      if (uid==null||prodId==null){
         return false;
      }

//    连接redis

//    Jedis jedis = new Jedis("192.168.188.99", 6379);
      JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
      Jedis jedis = jedisPoolInstance.getResource();

//    拼接key
//       库存key  用户key
//    库存
      String kc="stockProdId:"+prodId;
//    user
      String user="userProdId:"+uid;

//    监视
      jedis.watch(kc);

//    判断库存是否为空,如果为null,秒杀未开始

      String kcs = jedis.get(kc);
      if (kcs==null){
         System.out.println("秒杀还未开始");
         jedis.close();
         return false;
      }

//    判断用户是否秒杀

      /*
      * 谁秒杀成功,就取其值,因为其值存在set集合中,而且其特性是没有重复数据
      * 所以秒杀不成功,取到的值一定是null的
      * */
      if (jedis.sismember(user,uid)){
         System.out.println("抱歉,你已经秒杀过!");
         jedis.close();
         return false;
      }

//    如果库存数量为0,秒杀结束

      if (Integer.parseInt(kcs)<=0){
         System.out.println("商品已全部被秒杀");
         jedis.close();
         return false;
      }

//    组队
      Transaction multi = jedis.multi();
      multi.decr(kc);
      multi.sadd(user,uid);

//    执行
      List<Object> exec = multi.exec();

//    判断是否为空
      if (exec==null||exec.size() == 0){
         System.out.println("秒杀完毕");
         jedis.close();
         return false;
      }

//    秒杀过程
//       秒杀成功,库存减少,加入秒杀清单中

//    jedis.decr(kc);
//
//    jedis.sadd(user,uid);

      System.out.println("秒杀成功了");

      jedis.close();

      return true;
   }
  • 接下来可以在redis服务器中进行测试,然后查看库存是否为0.
  • 超卖问题和超时问题都解决了,但是是否存在执行了那么多次,库存却还有的问题呢?答案是肯定,比如,我进行了秒杀,刚进来发现版本号是1 ,但付款的时候版本却是2,导致我失败了。而我这次秒杀也是记在秒杀总次数里面的。所以就会造成库存遗留。版本号为什么会改变?当很多人同时进行一个操作,其中一个先成功了,修改了版本号,而那些没来得及提交的人却发现版本号不同了,就会秒杀不成功。造成遗留。

秒杀案例4.0

  • 解决遗留问题,可以使用lua脚本,该脚本是一种嵌入式脚本语言,可以调用c/c++,也可以被其调用,该脚本很常用于一些小型游戏的脚本代挂的开发。

package com.atguigu;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.LoggerFactory;

import ch.qos.logback.core.joran.conditional.ElseAction;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.Transaction;

public class SecKill_redisByScript {
   
   private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;

   public static void main(String[] args) {
      JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
 
      Jedis jedis=jedispool.getResource();
      System.out.println(jedis.ping());
      
      Set<HostAndPort> set=new HashSet<HostAndPort>();

   // doSecKill("201","sk:0101");
   }
   
   static String secKillScript ="local userid=KEYS[1];\r\n" + 
         "local prodid=KEYS[2];\r\n" + 
         "local qtkey='sk:'..prodid..\":qt\";\r\n" + 
         "local usersKey='sk:'..prodid..\":usr\";\r\n" + 
         "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + 
         "if tonumber(userExists)==1 then \r\n" + 
         "   return 2;\r\n" + 
         "end\r\n" + 
         "local num= redis.call(\"get\" ,qtkey);\r\n" + 
         "if tonumber(num)<=0 then \r\n" + 
         "   return 0;\r\n" + 
         "else \r\n" + 
         "   redis.call(\"decr\",qtkey);\r\n" + 
         "   redis.call(\"sadd\",usersKey,userid);\r\n" + 
         "end\r\n" + 
         "return 1" ;
          
   static String secKillScript2 = 
         "local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
         " return 1";

   public static boolean doSecKill(String uid,String prodid) throws IOException {

      JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
      Jedis jedis=jedispool.getResource();

       //String sha1=  .secKillScript;
      String sha1=  jedis.scriptLoad(secKillScript);
      Object result= jedis.evalsha(sha1, 2, uid,prodid);

        String reString=String.valueOf(result);
      if ("0".equals( reString )  ) {
         System.err.println("已抢空!!");
      }else if("1".equals( reString )  )  {
         System.out.println("抢购成功!!!!");
      }else if("2".equals( reString )  )  {
         System.err.println("该用户已抢过!!");
      }else{
         System.err.println("抢购异常!!");
      }
      jedis.close();
      return true;
   }
}

RDB之持久化技术

  • rdb指的是在指定时间间隔内将数据集快照写入硬盘中。
  • 写入:redis不会直接复制数据集到硬盘中,这样如果服务器挂掉,中间数据便会缺失。其在复制过程中主要先复制另一个一摸一样的子进程(fork)让这个子进程去完成自己的事情。该技术叫做写时复制技术。
  • redis的缺点是最后一次持久化过程可能会造成数据丢失。

AOF之持久化技术

  • 以日志的形式来记录每个写操作,读操作不记录,只允许追加文件不允许写入文件。
  • AOF默认不开启,当AOF与RDB共存时,系统默认读写AOF文件。所以如果你两个文件都开启的时候,你再重启服务端,会以AOF为基准读取数据,而如果你AOF里没有数据,也就读取不了,例如:keys * 为null。
  • 如果遇到AOF文件损坏,再bin目录下输入命令redis-check-aof --fix可进行修复。
  • 同步频率设置:
  • appendfsync always 始终同步,每次写入都进行记录,性能差,但数据能及时记录,更加完整。
  • appendfsync everysec 每秒记录一次,如果出错,该秒数据丢失。
  • appendfsync no 不进行同步,将保存操作交给操作系统。
  • 比RDB占用更多的磁盘空间,恢复备份速度要慢,每次读写都同步的时候,有一定的性能压力,存在个别bug,造成不能恢复。

主从复制

简介

  • 主从复制的提出是为了解决主机的压力而生。
  • 主机的数据同步到多个从机上,将读写操作分离,称为主从复制,简称一主多从。
  • 该模式可称为Master/Slave 模式,Master(主机)主要进行写操作,Slave(分机)主要进行读操作。
  • 主从复制限定只能有一个主机,可以有一个从机或者多个从机。
  • 特点:读写分离,性能拓展;解决容灾问题,当一个分机出现错误,可以寻找另一个分机进行操作。

搭建

  • 下面以搭建一主两从的案例。
  • 首先,在根目录下(即首次连接客户端后的目录)下输入mkdir /myredis 创建一个新的文件夹。
  • 然后输入命令cd /myredis 进入新建的文件夹,最后输入 cp /etc/redis.conf /myredis/redis.conf 将redis.conf复制过来。复制后可输入命令ll或者ls确认是否完成复制。
  • 输入指令 vi redis.conf 进入到该文件内,然后直接输入/appendonly 查找appendonly,其值修改为no,若原本为no则不用修改。:wq 可进行退出。
  • 配置一主两从,要有三个服务器,所以得有三个配置文件,即端口一定得三个不一样的。所以三个配置文件名可以将各自的端口号带上去。
  • 输入vi redis6379.conf 建一个新的文件夹,然后点击i进行输入模式,然后配置以下内容
include /myredis/redis.conf // 表示将redis.conf 内容包含进来,因为每开一个客户端都需要redis.conf ,配置很多,可以使用这样的方式将相同的配置拿过来
pidfile /var/run/redis_6379.pid //配置该文件自己的pid文件
port 6379 //配置该客户端自己的端口号
dbfilename dump6379.rdb //配置自己的dump.rdb文件
  • 其余的两个配置文件几乎都一样,只需要修改端口号便可以。
  • 修改完毕后,可以在/myredis 目录下,输入命令redis-server redis6379.conf启动相应端口的服务。全部启动后,输入ps -ef | grep redis 查看当前进程,可看见有三个进程,分别是自己创建的,如果显示有当前自己的进程,可进行杀除。
  • 下一步我们要为其分配主机和从机,首先,我们在/myredis 目录下,输入命令redis-cli -p port 可连接该端口号的服务器。例如redis-cli -p 6379 表示连接6379的服务器。依次的,我们可以再开启两个客户端窗口,进入myredis目录,同样的方式连接服务器。连接之前,比如进行上一步,启动各个端口号的服务器。
  • 都连接完毕后,我们可以再任何一个客户端窗口下输入info replication命令查看角色情况,可以看到,无论再任何一个端口的服务器。role角色都是为master,这表明我们并没有为其分配主从角色。
  • 因为本身便是主机,所以我们想要让哪台服务器变为从机,只要在其客户端窗口输入命令slaveof IP port 设置从机角色,例如slaveof 127.0.0.1 6379 中间的代表你的电脑ip 后者代表主机端口号。
  • 设置后,可以在主机当中输入info replication 可查看当前主机为master,从机有一个,端口号和ip地址都会显示出来,而在刚才输入命令的的客户端窗口输入info replication命令便会显示role角色变成了slave奴隶字样,表明该机器为从机.同样的,我们也可以为其余的从机设置。
  • 测试,前面说过。主写从读,我们在主机中set k1 v1 ,然后再从机尝试获取 get k1 是可以获取的,但是如果再从机中set k2 v2 设置值,会报以下错误
(error) READONLY You can't write against a read only replica.

一主两从特点

  • 搭建完毕后,我们也进行了测试,但为了显示一主两从的特点,我们可以再其中一个从机客户端中例如8081中输入shutdown将其进程杀掉,这就相当于现实当中的从机挂掉了。
  • 挂掉后,我们在从机中再设计几个值。
  • 设值完毕后,我们没有挂掉的从机肯定是可以查看到值的。但挂掉的从机再启动能不能获取主机的数据呢?
  • 为了验证这个问题,我们可以再次启动从机,启动从机后,这个从机会变成一个主机,所以要再次输入slaveof ip port命令进行认主,然后再进行服务器启动,之后进行连接才可以打开从服务器。
  • 打开后,我们可以尝试获取key,发现是可以完全获取主机的数据的。
  • 那如果是主服务器挂掉呢?其实一样,从服务器照样是从服务器,不会变成主服务器,当我们再次启动主服务器的时候,他之前的从服务器会再次认主。

原理

  • 从机第一次认主完毕后,会发送请求,主机将持久化的数据,也就是rdb文件夹交给从服务器。
  • 当主机有下次数据修改,其直接发送同步指令,不再需要从机请求,让从机的数据同步。

薪火相传

  • 例如数据结构的二叉树,从机可以给多个从机同步数据,而从机又能充当其他服务器的主机,继续分配从机,形成链式结构。
  • 但是如果某个从机挂掉了,该条延伸链将会彻底作废。

反客为主

  • 当主机挂掉后,所有从机得遵循一主两从的规则。当然如果输入命令slave no one可以让其中一个从机变成主机,反客为主。

哨兵模式

  • 前面我们说过反客为主,但是反客为主这个命令是要管理员自己输入的,如果主机挂掉了,没有及时将一台从机升级为主机,就会造成很大的麻烦。
  • 哨兵模式便是反客为主的自动版,当主机挂掉后,哨兵能够根据某个规则,例如投票的多少来升级哪台从机为主机。
  • 再打开一个客户端,进入到myredis 文件夹中,然后生成一个名为sentinel的文件,文件名称不能更改。然后再该文件里写入以下命令sentinel monitor mymaster 127.0.0.1 6379 1 该命令代表设置一个哨兵监视,监视外号为mymster的主机,最后是ip和端口号。而最后一个1代表,当有从机挂掉后,需要一个哨兵同意就可以反客为主。:wq退出保存。
  • 在myredis目录下启动哨兵:redis-sentinel sentinel.conf 。启动成功后,会有哨兵的监听信息。
  • 然后我们将主机shutdown后,会发现,其将某一个从机变成了主机,其他从机则作为这个新主机的从机,如果我们将旧的主机重新开启,而这个旧的主机也会变成新的主机的从机。

规则

  • 哨兵监视的规则是什么?我们可以退出到最开始的客户端目录,然后输入命令vi redis.conf 进入到该文件,然后直接输入/replica-priority 搜索该字段,会发现该字段有一个值,在哨兵模式中,如果新主登基,会先检查该值,该值越小,就选这个为主机。而如果该值是一样的,会选择和主机的数据同步量最高的分机进行登基,如果这个值还是一样,哨兵会选择redis每次启动随机生成的四位的runid最小的进行登基。

Jedis集成主从复制哨兵模式

private static volatile JedisSentinelPool jedisSentinelPool=null;
public static JedisSentinelPool getJedisPoolSentinelInstance() {
   if (null == jedisSentinelPool) {
      synchronized (JedisPoolUtil.class) {
         if (null == jedisSentinelPool) {
            Set<String> sentinel=new HashSet<>();
            sentinel.add("192.168.188.99:26379");
            JedisPoolConfig poolConfig = new JedisPoolConfig();
            poolConfig.setMaxTotal(200);
            poolConfig.setMaxIdle(32);
            poolConfig.setMaxWaitMillis(100*1000);
            poolConfig.setBlockWhenExhausted(true);
            poolConfig.setTestOnBorrow(true);  // ping  PONG
            jedisSentinelPool=new JedisSentinelPool("mymaster",sentinel,poolConfig);
         }
      }
   }
   return jedisSentinelPool;
}

缺点

  • 复制延迟,因为我们永远在主机上进行写的操作,而后将数据同步到从机上,所以这个过程永远有一个时间段,如果从机过多,这种时间延迟的缺陷会更加明显。

集群

简介

  • 为了解决redis存储不够而进行扩容的问题。
  • 分摊redis写操作的数据流,减少服务器的压力。
  • 另外,主从模式,薪火相传等都容易造成ip地址的改变,可能还需要改变回来,会造成不必要的麻烦。
  • 在redis3.0之前,普遍采用代理的方式去进行分摊的操作,比如一个商城网站,会有登录,商品展示等不同的模块,在redis3.0之前,让各个模块放在不同的redis之前,当客户端请求的时候通过一个代理机去访问各个redis。这样操作减少了用户与服务器的直接交互,效率更高,但是服务器和代理服务器都会宕机,所以肯定也得使用哨兵模式,所以如果有三个模块一个代理,那至少要有八个服务器(一个服务器,一个哨兵),资源消耗会变大。
  • 能不能消除代理的模式?集群便通过这个原理,将代理取消,每个服务器都不是中心(去中心化),但这些服务器会串成图结构,都能互相访问,用户直接请求服务器,服务器发现请求不对,转发给另外的服务器去解决。

搭建

  • cd /myredis 进入该目录,然后输入ll或者ls命令查看当前文件夹的文件,输入rm -rf dump63_ 删除所有带有dump63_的文件。
  • 之前我们创建过几个端口号的文件,现在要搭建集群,我们至少要创建六个不同端口号的文件,但是文件内容与之前的需要稍微更改。
include /myredis/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 //设置超时时间

文件内容一定要一摸一样,不同端口号的文件里面有端口号字样的都得修改成该文件的端口号,不能出错。

  • :wq退出文件编辑,为了方便,将还没修改号的两个端口号文件删除,随后输入cp redis6379.conf redis6380.conf 直接将原来的配置复制出一个新的文件,我们修改端口号,再复制出五份,一个六份。但还没完,每个端口号都得修改。例如,我们修改端口号为6380的文件,我们输入命令vi redis6380.conf 进入该文件,然后直接输入:%s/6379/6380 代表将6379所有的字样都改成6380。同样的,也可以用这个方式快速修改其他配置文件。
  • 结束所有的文件编辑后,启动各个配置的文件的服务,例如:redis-server redis6379.conf 然后输入命令ps -ef | grep redis 查看所有的redis服务启动情况。
  • 合成集群:启动各个配置文件后,输入cd /opt 进入到我们安装redis的目录,然后输入ls查看所有文件,可查看redis版本信息,最后进入对应版本的redis文件,例如输入cd redis-6.2.6 进入6.2.6版本的文件中,再该文件中,我们可以看到有一个src目录,该目录包含很多指令,我们输入cd src进入该目录。
  • 接下来,我们要输入以下命令
redis-cli --cluster create --cluster-replicas 1 192.168.188.99:6379 192.168.188.99:6380
192.168.188.99:6381 192.168.188.99:6389 192.168.188.99:6390 192.168.188.99:6391

redis-cli --cluster 为启动集群命令 create --cluster-relicas 1 代表使用最基本的方式创建主从模式,比如这里有三个主机,这个三个主机分别对应一个从机,所以有六个服务器。
注意:复制这个命令输入到控制台的时候,要将这个命令再文本中搞成一行,当然中间有空格的还是得有空格。而且,这个命令的ip地址是你的客户端的ip地址,不是你电脑的ip地址。
合成集群后,不要清空客户端,要复制下[OK] All 16384 slots covered.的话语,待会会用到。

  • 连接集群,搭建集群完毕后,再src目录下可以输入命令redis-cli -c -p port 连接集群中一条主从,例如,redis-cli -c -p 6379 连接端口号为6379的主从。然后输入命令cluster nodes 可查看集群各主从服务器的关系。
  • 注意:值得注意的是,这里我们只是作为测试,但是在实际的开发中,我们要做到每个节点的ip都不能相同,每个主从的ip也不能相同。

故障修复

  • 该集群主从故障修复,遵循哨兵模式。
  • 那如果主从都产生了故障呢?取决于redist.conf中的一个配置,cluster-require-full-coverage为yes的时候,集群中任何一个主从都挂掉了,其他主从不能操作,反之为no,则不影响其他主从。

slots

  • 在上面的搭建过程中,我们拿到了16384的这个值,该值代表在redis中,一共有0-16383个插槽,这些插槽的作用在于存放每次设置key的值。
  • 当key的值产生,redis会根据CRC16(key)%算法来计算key要存放的插槽位置,这个插槽位置会对应在一台主机中。在前面输入命令cluster nodes可以查看集群主从的情况,在每个主机里,我们都可以看到其分配的插槽值范围。
  • 例子:我们连接6379的主机后,在里面设置一个值,会出现下面的提示
Redirected to slot [12706] located at 192.168.188.99:6381

该提示表示,该值放在12706的插槽里,端口号6381的主机。虽然我们是在6379的主机上设置值的,但是因为集群的特点,该计算出的插槽位置不符合自己的插槽范围后,该主机就会交给适合的去做。
但上面我们只能在设置单个值的时候存放进去。比如,set k1 v1 是可以存放进去的,但是如果mset k2 v2 k3 v3 会报(error) CROSSSLOT Keys in request don’t hash to the same slot的错误,该提示说明无法计算插槽值。所以在集群中要想一下设置多个值,只能用”组“的方法,例如mset k2{user} v2 k3{user} v3利用该方法便不会报错,其计算插槽值的时会利用user来进行计算。

  • 在redis中,我们存储了key还想计算其插槽值,可输入命令cluster keyslot key。同样的,知道插槽值,可计算该插槽内有多少个key:cluster countkeysinslot 插槽值(插槽值不能超过本主机的插槽值范围。)。命令cluster getkeysinslot 插槽值 count 可返回该插槽内指定数量key的值。

Jedis连接集群

  • 可以从任何一个主机端口进入
public static void main(String[] args) {
    HostAndPort hostAndPort = new HostAndPort("192.168.188.99",6379);
    JedisCluster jedisCluster = new JedisCluster(hostAndPort);
}

缺点

  • 多键的事务不被支持,也不支持lua脚本。
  • 由于集群的概念出现的较晚,目前很多公司还是用以前的方式去操作。

缓存穿透

  • 用户浏览网页的过程:由浏览器发送请求给服务器,服务器会先在缓存中拿数据,而缓存的数据来源于对数据库的数据同步。当服务器的压力增大,对缓存的访问量就会增大,这个时候缓存和数据库的数据同步不过来,服务器就会去查询数据库,数据库查询一旦增大,就会造成崩溃。
  • 解决方案:
  • 空值缓存,如果查询不到数据,我们直接将其定义为null,并设置其过期时间。
  • 设置可访问的白名单,在redis新的版本中,提供bitMaps类型,我们可以使用该类型的偏移量定义一个可访问的白名单,如果访问的id不存在白名单内,便拦截其访问。
  • 采用布隆过滤器:该过滤器的概念是由1970年布隆提出来的,其基本思量和bitMaps类型,不过效率更高。
  • 结合实际运维工程师解决。

缓存击穿

  • 数据库访问压力瞬间变大;redis里并没有出现key过期,redis平稳运行,但数据库崩溃。
  • 例子:如果redis里有一个key过期了,而某次突然增大访问,该key要用到,服务器又开始查询数据库。
  • 解决方案:
  • 预先设置和监控热门数据,将过期时间增加。
  • 设置锁。

雪崩

  • redis出现大量过期key,无法查询缓存数据,转而查询数据库,数据库压力增大,反过来让服务器崩溃。
  • 解决办法:
  • 构建多级缓存,不仅仅是redis。
  • 使用锁和队列,但效率低,不适用于高并发。
  • 设置过期标志,更新缓存。
  • 对不同缓存设置不同的过期时间,更新缓存。

分布式锁

简介

  • 在分布式架构和集群中,因为去中心化的缘故,一把锁只能对应一个机器,而要想在整个集群和架构中让锁共享,对每个机器都有效,就是分布式锁解决的问题。
  • 在redis里,setnx key value便用到了锁的结构,如果你利用此命令为key设置了一个value,当你第二次再使用次命令为相同的key设置值的时候,会设置失败。只有等该设置的锁释放了(del key),再为相同的key才能设置。要想让这个锁自己释放,可以为key设置过期时间。但值得注意的是,如果上锁和设置过期时间不再同一时刻进行,那么当上锁后出现异常而没有设置过期时间便会让该锁一直处于锁住的状态。比如:set k1 v1 nx ex 10 ,nx表示为k1上锁,继续后面的ex表示同时为k1设置过期时间。

问题

  • 锁释放错误:一把锁虽然可以在整个分布式架构中共享,但是实际使用起来只能由一台机器(a)使用,但如果a在操作的过程中出现服务器延迟,而锁的过期时间也到了,这个时候锁会自动释放。自动释放后,b服务器抢到了这把锁使用上了,但在b的操作过程中,a也刚好能正常操作了,而a操作完的时候会手动释放锁,因为是共享的一把锁,所以a会找到当前这个锁释放掉,但这个锁其实已经在b里头了。
  • 解决方法很简单,为每次上锁的时候设置uuid值,给每台服务器用锁一个唯一标识,在释放的时候再判断uuid是否一致便可以了。
  • 删除错误:虽然释放错误解决了,但上锁和解锁的操作缺乏原子性,所以在此过程还会出现错误,加入a机器判断uuid成功后,可以删除,但在删除的过程中服务器延迟了,刚好延迟的时间内锁又自动过期释放了,b就会抢到这把锁,而b刚好将锁锁上,a的删除操作又可以进行了,就会将b的锁也删了。注意:这里已经不关心uuid的问题,因为a在删除锁之前已经判断uuid是正确的。
  • 解决办法:使用lua脚本将判断uuid是否正确的过程描述代替原来的描述,因为lua脚本原子性的操作,该过程不会被打算。