Redis学习笔记(持续更新)

Redis 6 (NoSQL数据库)

NoSQL数据库

​ NoSQL:Not Only SQL,意为不仅仅是SQL,泛指非关系型数据库,NoSQL不依赖业务逻辑方式存储,而以简单的Key-Value模式存储,因此大大增加了数据库的扩展能力。

redis是单线程+多路IO复用的方式:联想通过黄牛买火车票的例子

  • 不遵循SQL标准:有自己的操作标准
  • 不支持ACID:原子性、一致性、隔离性、持久性
  • 远超于SQL性能

适用场景:

  • 对数据高并发的独写
  • 海量数据的独写
  • 对数据高可扩展性

不适用场景:

  • 需要事务支持
  • 基于SQL的结构化查询存储,处理复杂的关系,需要即席查询

总结:用不着SQL的和用了SQL也不行的情况,请考虑用NoSQL

一些NoSQL数据库

Memcache:

  • 很早出现的NoSQL数据库
  • 数据都在内存中,一般不持久化
  • 支持简单的Key-Value模式,支持类型单一
  • 一般是作为缓存数据库辅助持久化的数据库

Redis:

  • 几乎覆盖了Memcache的绝大部分功能
  • 数据都在内存中,支持持久化,主要用作备份恢复
  • 除了支持简单的Key-Value模式,还支持多种数据结构的存储,比如list、set、hash、zset等
  • 一般是作为缓存数据库辅助持久化的数据库

MongoDB:

  • 高性能、开源、模式自由(schema free)的文档型数据库
  • 数据都在内存中,如果内存不足,把不常用的数据保存到硬盘
  • 虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能
  • 支持二进制数据及大型对象
  • 可以根据数据的特点替代RDBMS,成为独立的数据库,或者配合RDBMS,存储特定的数据

安装

1、下载redis安装压缩包

2、将压缩包通过Xftp放进Linux虚拟机中

3、进入该文件中的包所在的位置在XShell软件中解压

tar -zxvf 解压文件名

4、解压完成后进入解压后的目录

5、输入make命令,使所有文件编译为C语言环境(需要预先安装C语言环境:yum install gcc)

6、安装redis 输入:make install

安装后默认文件存储位置在:usr/local/bin

里面一共有下面6个文件:

  1. redis-benchmark :性能测试工具,可以在自己服务器运行,看看自己服务器性能
  2. redis-check-aof :修复有问题的AOF文件
  3. redis-check-rdb :修复有问题的dump.rdb文件
  4. redis-cli :客户端操作入口
  5. redis-sentinel :Redis集群使用
  6. redis-server:Redis服务器启动命令

启动

前台启动(不推荐)

前台启动,命令行窗口不能关闭,否则服务停止

启动命令:

redis-server

停止命令:

ctrl+c

后台启动

首先将解压的Redis文件中的Redis.confi文件复制到另一个文件夹下

然后打开复制的文件:

vi redis.conf

进入到文件后:

将daemonize no 改为 yes :该修改表示准许让其在后台启动

然后进入到 usr/local/bin目录下

输入

redis-server /redis/redis.conf(这后面的路径是刚才复制的文件路径)

关闭

单实例关闭

redis-cli shutdown

或者进入中断后直接输入shutdown

多实例关闭

指定端口关闭

kill -9 端口号

Key键操作

set key value 简单的key value设置
keys * 查看所有的key
exists key 查看某个key是否存在 1:存在 0:不存在
type key 查看key的类型
del key 删除指定的key的值
unlink key根据value选择非阻塞删除:仅将keys从keyspace元数据中删除,真正删除会在后面后续异步操作
expire key time 给key设过期时间
ttl key 查看key过期还有多少秒过期:-1表示永不过期,-2表示已经过期
select n ( 16>N>=0 ):查询有多少个库,默认16个库
dbsize:查询当前库中有多少个key
flushdb:清空当前库
flushall:通杀全部库

常用数据类型

String

String类型是二进制安全的,意味着Redis的String可以包含任何数据,比如jpg图片或序列话对象

一个Redis字符串value最大可以是512MB

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中存储的值增减,可以自己设置步长

重复设置相同的key会将之前的覆盖

mset key1 value1 key2 value2 # 同时设置多个值
mget key1 key2 #同时或取多个值
msetnx key1 value1 key2 value2 #同时设置多个值,当且仅当给定的key不存在 ,原子性,有一个失败则都失败

getrange key 起始位置 结束位置 # 获得值得范围,类似java得substring
setrange key 起始位置 value # 用value覆写 key 所存储的字符串,从其实位置开始
setex key 过期时间 value #设置键值的同时,设置过期时间,单位秒
getset key value # 设置新值同时替换旧值

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

image-20210507114327554

List

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

​ 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能比较差

常用命令:

lpush/rpush key value1 value2 value3 ... #从左边/右边插入一个或多个值
lpop/rpop key #从左边/右边吐出一个值:值在键在,值亡键亡
rpoplpush key1 key2 #从key1列表右边吐出一个值插入key2列表左边
lrange key start stop #按照索引下标获得元素(从左到右)lrange key 0 -1 :取到列表里面的所有值

lpush的原理类似于弹夹压子弹,或者队列,而不是第一个进入的就在索引的第一个

lindex key index #根据索引下标获得元素(从左到右)
llen key #获取列表长度

linsert key before value newvalue #在value的 前面 插入newvalue
linsert key after value newvalue #在value的后面插入newvalue

lrem key n value #从左边删除 n 个值为 value 的值 (从左到右)
lset key index value #将列表key下标为index的值替换成value

数据结构

image-20210507173026656

image-20210507173300220

Set

​ Redis Set 对外提供的功能于list类似是一个列表的功能,特殊之处是在于set可以自动排重 ,当你需要存储一个列表数据,又不希望出现重复数据的时候,set是一个很好的选择,并且set提供了判断某个成员是否在set集合内的重要接口,这个也是list所不能提供的

​ Redis的Set是String类型的无序集合,他底层其实是一个value为null的hash表,所以添加、删除、查找的复杂度都是O(1)

常用命令:

sadd key value1 value2 ... #将一个或多个元素加入到集合中,已经存在的元素将被忽略
smembers key #取出key中的值
sismember key value #判断集合key是否为含有该value的值,有返回1,没有则为0
scard key #但会该集合元素的个数
srem key value1 value2 ... #删除集合中的某个元素
spop key #随机从该集合pop出一个值
srandmember key n #随机从该集合中取出n个值,但不会从集合中删除
smove key1 key2 value #把key1集合中的value从key1集合移动到key2集合
sinter key1 key2 #返回两个元素的交集元素
sunion key1 key2 #返回两个元素的并集元素
sdiff key1 key2 #返回两个集合的差集元素(key1中的,不包含key2中的)

数据结构:

​ Set数据结构是dict字典,字典是用哈希表实现的

​ Java中的HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象,Redis的set结构也是一样的,它的内部也是用hash结构,所有的value都指向同一个内部值

Hash

image-20210508172124021

image-20210508172242526

常用命令:

image-20210508172709148

删除

hdel key filed #删除key中的filed字段
hgetall key #获取所有字段和内容

数据结构:

Hash类型对应的数据结构是两种:zipList(压缩列表),hashtable(哈希表)当filed-value长度较短且个数较少时,使用ziplist,否则使用hashtable

Zset(sorted set)

​ Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

​ 不同之处时有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。

集合的成员是唯一的,但是评分是可以重复的

- 因为元素是有序的,所以你也可以很快的根据评分或则次序(position)来获取一个范围的元素
- 访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的只能列表

常用命令:

zadd key score1 value1 score2 value2 ... #将一个或多个member元素及其score值加入到有序集key当中
zrange key start stop [WITHSCORES] #返回有续集key中,下标在start和stop之间的元素,带WITHSCORE,可以让分数一起和值返回到结果集
zrangebyscore key min max [withscores][limit offset count] #返回有序集中,所有score值介于min和max之间(包括等于min和max)的成员
                                                           #有序集成员按score值递增(从小到大)次序排列
zrevrangebyscore key max min [withscores][limit offset count] #同上,改为从大到小排序
zincrby key increment value #为元素的score加上增量
zrem key value  #删除该集合下,指定的元素
zcount key min max #统计该集合,分数区间内的元素个数
zrank key value #返回该值在集合中的排名,从0开始

数据结构:

image-20210509104249437

image-20210509104443272

跳跃表

image-20210509104545352

image-20210509104617575

​ (2)跳跃表

image-20210509104724290

新数据类型

Bitmaps(0或1)

image-20210509132209431

image-20210509132256271

命令:

setbit key offset value #设置Bitmaps中某个偏移量(offset)的值(0或1) offset:偏移量从0开始

image-20210509132654835

在第一次初始化Bitmaps时,加入偏移量非常大,那么整个初始化过程会比较慢,可能会造成Redis的阻塞

getbit key offset #获取Bitmaps中某个偏移量的值 offset从0开始算

bitcount:统计字符串被设置为1的bit数,一般情况下吗,整个字符串都会被进行计数,通过格外的start和end参数,可以让计数只在特定的位上进行,start和end参数的设置,都可以使用负数值:比如-1表示最后一个位,而-2表示倒数第二个位,start、end是指bit组的字节的下标数,二者皆包含

bitcount key [start end] #统计从start到end字节比特值位1的数量
bitop and/or/not/xor destkey key #做多个Bitmaps的交集并集非异或,并将结果保存在destkey中
bitop and kand key1 key2 #这个意思就是对key1和key2进行逻辑与操作将结果存如kand里面

与Set对比:

image-20210509135322460

HyperLogLog(存基数不存值)

image-20210509135808001

image-20210509135914263

命令:

pfadd key element [element...] #添加指定元素到HyperLogLog中,会去重
pfcount key [key...] #计算HLL的近似基数,可以计算多个HLL,说白了就是计算key中元素个数
pfmerge destkey sourcekey [sourcekey...] #将一个或多个HLL合并后的结果存储在另一个HLL中

Geospatial(经纬度)

image-20210509140818658

geoadd key 经度 维度 member [...] #添加地理位置
geoadd china 121.47 31.23 shanghai 

有效的经度从-180度到180度,有效的维度从-85到85度 当坐标位置超出指定范围时会报错,同样具有去重

geopos key member [...] #获取指定地区经纬度
geodist key member1 member2 [m/km/mi/ft] #获取两个位置之间的直线距离 m:米 km:千米 mi:英里 ft:英尺
georadius key 经度 维度 半径 m/km/ft/mi #以给定的经纬度为中心找出某一半径内的元素

发布和订阅

什么是发布和订阅:

​ Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息

​ Redis客户端可以订阅任意数量的频道

1、打开一个客户端订阅channel1

subscribe channel1

image-20210509113114994

2、打开另一个客户端,给channel1发布消息hello

publish channel1 hello

image-20210509113131325

返回订阅者数量

3、打开第一个客户端可以看到发送的消息

image-20210509113205406

Jedis

用Java操作redis

依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.6.0</version>
</dependency>
public static void main(String[] args) {
    //创建Jedis对象 host:ip地址 port:端口号
    Jedis jedis = new Jedis("192.168.6.128",6379);
    //测试
    String value = jedis.ping();
    System.out.println(value);
}

测试连接是否成功,如果没有ping通,可能是linux防火墙没有关闭

systemctl stop firewalld

在linux关闭防火墙,或者开放端口

//操作key
@Test
public void keyTest(){
    //创建Jedis对象 host:ip地址 port:端口号
    Jedis jedis = new Jedis("192.168.6.128",6379);
    //添加
    jedis.set("name","lucy");
    //获取
    String name = jedis.get("name");
    System.out.println(name);
    Set<String> keys = jedis.keys("*");
    for (String key : keys){
        System.out.println(key);
    }
    jedis.close();
}

模拟实现手机验证码

image-20210511141006068

package jedis;
import redis.clients.jedis.Jedis;
import java.util.Random;
public class PhoneTest {
    public static void main(String[] args) {
//        verifyCode("10000000000");
        getRedisCode("10000000000","156740");
    }
    /**
     * 验证码校验
     * 场景:输入验证码
     * @param phone
     * @param code
     */
    public static void getRedisCode(String phone,String code){
        Jedis jedis = new Jedis("192.168.6.128",6379);
        String codeKey = "VerifyCode" + phone + ":code";
        String redisCode = jedis.get(codeKey);
        if (redisCode.equals(code)){
            System.out.println("成功");
        }else {
            System.out.println("失败");
        }
    }
    /**
     * 每个手机每天只能发送三次,验证码放到redis中,设置过期时间
     * 场景:输入手机号,生成验证码
     * @param phone
     */
    public static void verifyCode(String phone){
        //连接redis
        Jedis jedis = new Jedis("192.168.6.128",6379);
        //拼接key 手机发送次数key
        String countKey = "VerifyCode" + phone + ":count";
        //验证码key
        String codeKey = "VerifyCode" + phone + ":code";
        //每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if (count == null){
            //没有发送次数,第一次发送,设置发送次数为1
            jedis.setex(countKey,24*60*60L,"1");
        }else if (Integer.parseInt(count) <= 2){
            //发送次数+1
            jedis.incr(countKey);
        }else if (Integer.parseInt(count) > 2){
            //发送三次,不能再发送
            System.out.println("今天发送次数已经超过三次,不能再发送");
            jedis.close();
        }
        //发送验证码放到redis中去
        String vcode = getCode();
        jedis.setex(codeKey,120L,vcode);
        jedis.close();
    }
    /**
     * 随机生成6位数字验证码
     * @return
     */
    public static String getCode(){
        Random random = new Random();
        String code = "";
        for (int i = 0;i < 6;i++){
            int rand = random.nextInt(10);
            code = code + rand;
        }
        return code;
    }
}

Springboot整合Redis

依赖引入

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.x集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
</dependency>

在配置yaml文件中配置连接参数

spring:
  redis:
    host: 192.168.6.128
    port: 6379

添加redis通用配置类:

/**
 * @Description:Redis通用配置类
 * @Author 16221
 * @Date 2020/4/23
 **/
@Configuration
public class RedisConfig {
    @Bean
    //不指定id的话bean 的id就是方法名
    //返回结果就是spring中管理的bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();

        //ObjectMapper 指定在转成json的时候的一些转换规则
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //把自定义objectMapper设置到jackson2JsonRedisSerializer中(可以不设置,使用默认规则)
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        //RedisTemplate默认的序列化方式使用的是JDK的序列化
        //设置了key的序列化方式
        template.setKeySerializer(new StringRedisSerializer());
        //设置了value的序列化方式
        template.setValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }
}

Controller:

通过自动注入:
    @Autowired
    private RedisTemplate redisTemplate;
    //设置值
	redisTemplate.opsForValue().set("key","value");
	//取值
	Object name = redisTemplate.opsForValue().get("key");

事务

redis事务是一个单独得隔离操作:事务中得所有命令都会序列化、按顺序执行,事务在执行过程中吗,不会被其他客户端发送来得命令请求所打断

串联多个命令,防止插队

Multi、Exec、discard

从输入Multi开始,输入得命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis将之前得命令队列中的命令一次执行,如果输入discard就是放弃组队,命令就不会执行。

image-20210513135548784

错误处理

  • 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消
  • image-20210513140120611
  • 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,为其他的命令都会执行,不会回滚
  • image-20210513140247833

事务冲突问题

悲观锁、乐观锁

悲观锁: 每次操作之前,先对操作资源上锁

乐观锁:不对资源上锁,在资源上加上标识,对操作该资源的行为也加上相应标识,如果标识比对相等,则可以操作,资源发生更改,会更改标识(适合多读的操作)

image-20210513141316937

Watch key(乐观锁)

在执行multi之前,先执行watch key1 [key2] 可以监视一个或多个key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断

unwatch:

  • 取消watch命令对所有key的监视
  • 如果在执行watch命令之后,exec命令或discard命令先被执行了的化,那么就不需要再执行unwatch了

Redis事务三特性

  1. 单独的隔离操作
    • 事务中的所有命令都会序列化、按顺序的执行、事务在执行过程中,不会被其他客户端发送来的命令请求所打断。
  2. 没有隔离级别的概念
    • 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
  3. 不保证原子性
    • 事务中如果有一条命令执行失败,其他的命令仍然会被执行,没有回滚

秒杀案例

image-20210514163853948

简单秒杀过程:

//秒杀过程
public static boolean doSecKill(String uid,String prodid)throws IOException{
    //1、uid和prdoid非空判断
    if(uid == null || prodid){
        return false;
    }
    //2、连接redis
   	Jedis jedis = new Jedis("192.xxx.xx.xxx",6379);
    //3、拼接key
     //3.1库存key
     String kcKey = "sk:" + prodid + ":qt";
   	 //3.2秒杀成功用户列表key
     String userKey = "sk:" + prodid + ":user";
    //4、获取库存,如果库存null,秒杀还没有开始
    String kc = jedis.get(kckey);
    if(kc = null){
        System.out.println("秒杀还没有开始,请等待");
        jedis.close();
        return false;
    }
    //5、判断用户是否重复秒杀操作
    if(jedis.sismember(userKey,uid)){
        System.out.println("已经秒杀成功了,不能重复秒杀");
        jedis.close();
        return false;
    }
    //6、判断如果商品数量小于1,秒杀结束
    if(Integer.parseInt(kc) < 1){
        System.out.println("秒杀已经结束");
        jedis.close();
        return false;
    }
    //7、秒杀过程
     //7.1库存-1
     jedis.decr(kcKey);
     //7.2把秒杀成功的用户添加清单里面
     jedis.sadd(userKey,uid);
   	 System.out.println("秒杀成功了..");
     jedis.close();
    return true;
}

ab工具模拟并发

安装:yum install httpd-tools

ab --help 查看帮助

个别语法:

ab -n 1000(请求数量) -c 100(请求并发数) -p ~/postfile -t (html中的encotype) http://ip地址/路径

连接超时问题和超卖问题

连接超时问题:

创建连接池

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(20);//设置最大连接数
                    poolConfig.setMaxIdle(32);
                    poolConfig.setMaxWaitMillis(100*1000);//设置等待时间
                    poolConfig.setBlockWhenExhausted(true);//是否进行等待
                    poolConfig.setTestOnBorrow(true);
                    jedisPool = new JedisPool(poolConfig,"192.xxx.xxx.xxx",6379,60000);
                }
            }
        }
        return jedisPool;
    }
    //释放资源
    public static void release(JedisPool jedisPool,Jedis jedis){
        if(null != jedis){
            jedisPool.returnResource(jedis);
        }
    }
}

将上面简单秒杀第二步的代码进行改造

//2、连接redis
//Jedis jedis = new Jedis("192.xxx.xx.xxx",6379);
//通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();

超卖问题:

修改简答秒杀代码(加*号的注释为修改的代码)(乐观锁解决)

//秒杀过程
public static boolean doSecKill(String uid,String prodid)throws IOException{
    //1、uid和prdoid非空判断
    if(uid == null || prodid){
        return false;
    }
   	//2、连接redis
	//Jedis jedis = new Jedis("192.xxx.xx.xxx",6379);
	//通过连接池得到jedis对象
	JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
	Jedis jedis = jedisPoolInstance.getResource();
    //3、拼接key
     //3.1库存key
     String kcKey = "sk:" + prodid + ":qt";
   	 //3.2秒杀成功用户列表key
     String userKey = "sk:" + prodid + ":user";
    //*监视库存
    jedis.watch(kcKey);//如果执行事务之前,kcKey的值被其他事务所改动,就中断当前事务
    //4、获取库存,如果库存null,秒杀还没有开始
    String kc = jedis.get(kckey);
    if(kc = null){
        System.out.println("秒杀还没有开始,请等待");
        jedis.close();
        return false;
    }
    //5、判断用户是否重复秒杀操作
    if(jedis.sismember(userKey,uid)){
        System.out.println("已经秒杀成功了,不能重复秒杀");
        jedis.close();
        return false;
    }
    //6、判断如果商品数量小于1,秒杀结束
    if(Integer.parseInt(kc) < 1){
        System.out.println("秒杀已经结束");
        jedis.close();
        return false;
    }
    //*7、秒杀过程
    Transaction multi = jedis.multi();//使用事务
    //组队操作
    mutli.decr(kcKey);
    jedis.sadd(userKey,uid);
    //执行
    List<Object> results = multi.exec();
    if(results == null || results.size() == 0){
        System.out.println("秒杀失败了");
        jedis.close();
        return false;
    }
   	 System.out.println("秒杀成功了..");
     jedis.close();
     return true;
}

库存遗留问题(Lua脚本)

乐观锁造成库存遗留的问题:用户秒杀成功后,对数据进行修改,版本号发生改变,其余抢到的用户再对数据进行操作,版本号不一致,就失败了,所有最后真正抢到的用户数量少

  1. Lua脚本解决

image-20210515141249375

image-20210515141337916

lua脚本示例

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

如何使用Lua脚本:

将上面的脚本代码复制下来装在一个String类型的变量中

static String secKillScript = "Lua脚本";
String shal = jedis.scriptLoad(secKillScript);//将脚本代码字符串用scriptLoad代码编译一次
Object result = jedis.evalsha(shal,2,userid,prodid);//2为Key的数量
String reString = String.valueOf(result);//将返回的值转换为字符串,返回的就是Lua脚本中设置的返回值

注意:以上脚本运行了就是对整个事务的操作了,不需要再进行jedis事务操作了

持久化操作

RDB(Redis DataBase)

​ 在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时将快照文件直接读到内存里

备份执行:

image-20210515151000158

dump.rdb文件

在哪个目录下启动redis服务,就在哪个目录下生成dump.rdb文件

save VS bgsave :

  • save :save时只管保存,其他不管,全部阻塞。手动保存。不建议
  • bgsave:Redis会在后台异步进行快照操作,快照同时好可以响应客户端请求

RDB最后一次持久化后的数据会丢失的原因:

image-20210515152722472

备份

  1. 先通过config get dir 查询rdb文件的目录
  2. 将*.rdb的文件拷贝到别的地方

恢复:

  • 关闭Redis
  • 把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
  • 启动Redis,备份数据会直接加载

优势:

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高更适合使用
  • 节省磁盘空间
  • 恢复速度块

劣势:

  • Fork的时候,内存中的数据被克隆了一份,大致两倍膨胀性需要考虑
  • 虽然Redis在fork的时候使用了写时拷贝技术,但是如果数据庞大还是比较消耗性能
  • 在备份周期在一定间隔时间做一次备份,如果Redis以为shutdown的话,就会丢失最后一次快照后的所有备份

AOF(Append Of File)

image-20210518113500821

只记录写操作,不记录读操作

AOF默认不开启:

​ redis.conf配置文件默认属性appendonly 为no 如果要开启将no改为yes

appendfilename "appendonly.aof" 属性设置的是自动生成的aof文件名

生成路径都是redis启动路径生成

AOF和RDB同时开启,默认读取AOF中的数据

备份

AOF备份操作与RDB操作一样:

  • 将AOF文件复制
  • 当之前的文件删除掉后
  • 将复制文件名字修改为之前的名字
  • redis会自动识别AOF文件

异常恢复

如遇到AOF文件出现异常,通过 :redis-check-aof--fix appendonly.aof 进行恢复

同步频率设置

配置文件中:

  • appendfsync always :始终同步,每次Redis的写入都会立即计入日志(性能较差,但数据完整性比较好)
  • appendfsync everysec : 每秒同步,每秒计入日志一次,如果宕机,本秒的数据可能丢失
  • appendfsync no : redis不主动同步,把同步时机交给操作系统

Rewrite压缩

image-20210518140423597

主从复制

是什么?

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

image-20210518141444987
  • 读写分离
  • 容灾快速恢复(一台服务器挂掉,快速切换另一台从服务器)

操作

  1. 创建一个myredis文件夹 :mkdir /myredis
  2. 复制redis配置文件(redis.conf): cp /redis/redis.conf /myredis/redis.conf ;在复制的文件中把appendonly值改为no
  3. 配置一主两从,创建三个配置文件:redis6379.conf , redis6380 , redis6381.conf 直接vi 文件名
  4. 在三个配置文件中写入内容 include /myredis/redis.conf 引入配置
  5. 在三个配置文件中写入内容 pidfile /var/run/redis_6379.pid
  6. 在三个配置文件中修改端口号 port 6379 (对应每个文件的端口号)
  7. 在三个配置文件中配置rdb文件名称 dbfilename dump6379.rdb

启动:

在myredis目录下用redis-server redis63xx.conf命令对三台服务器进行启动

连接服务器使用 redis-cli -p 端口号

ps -ef | grep redis :查看启动状况

info replication :查看主从机配置 image-20210519174014863

配置从机

slaveof ip port

例如:

在从机6380、3681上输入主机端口

slaveof 127.0.0.1 6379 

主机挂掉,重启就行

从机挂掉,需要重新设置主从

一主二仆及复制原理

注意:从服务器关掉后,重新启动,不会自动变为从服务器,需要重新设置;

​ 主服务器挂掉后,重新启动,还是主服务器

复制原理:

  1. 当从服务器连上主服务器后,从服务器向主服务发送进行数据同步消息
  2. 主服务器接到服务器发送过来同步消息,把主服务器数据进行持久化rdb文件,把rdb文件发送从服务器,从服务器拿到rdb进行读取
  3. 每次主服务器进行写操作过后,和从服务器进行数据同步

薪火相传

image-20210524171433121

image-20210524172030110

之前所有从服务器都连接主服务器,造成主服务器压力太大,现在将他们变成链条式,一个从服务器连接另一个从服务器,前一个从服务器做后一个从服务器的主服务器

连接也是使用 slaveof ip 从服务器port

特点:1. 某一个slave宕机,后面的slave都没法备份,主机挂了,从机还是从机,无法写数据了

反客为主

主服务器挂掉了,后面的从服务器变为主服务器

slaveof no one 将从机变为主机

哨兵模式(反客为主自动版)

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

image-20210524200106570

操作:

  1. 自定义/myredis目录下新建sentinel.conf 文件,名字不能错
  2. 在文件中配置内容sentinel monitor mymaster 127.xxx.xxx.xxx port 1 ;其中mymaster监控对象起的服务器名称,1为至少有多少个哨兵同意迁移的数量:如果为1,一个哨兵同意就切换,如果为2,就必须两个哨兵同意才切换
  3. 启动哨兵 :myredis-sentinel /myredis/sentinel.conf 启动

注意:在哨兵模式中主服务器挂掉后,重新启动,会自动变成从服务器

复制延迟:由于所有的写操作都是在Master上操作,然后同步更新到slave上,,所以从Master复制到slave上机器有一定的延迟,当系统 很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使整个问题更加严重

选择主服务器优先级问题:

  1. redis.conf配置文件中,有 replica-priority xxx 值,xxx值越小,优先级越高
  2. 偏移量:获得原主机数据最全的
  3. 选择runid最小的(每个redis服务启动后,都会随机生成一个40为的runid)

在java中用连接池将哨兵服务器加入进去:

image-20210524205232774

集群

无中心化集群取代代理主机模式

image-20210525110854605

搭建集群

  1. 删除掉之前的rdb文件,防止产生冲突rm -rf 文件名

  2. 配置文件保留这部分image-20210525112710639

  3. 在配置文件中配置

    1. cluster-enabled yes :打开集群模式
    2. cluster-config-file nodes-6379.conf 设定节点配置文件名
    3. cluster-node-timeout 15000 设置节点失联时间,超过该时间,集群自动进行主从切换
  4. 删掉其他配置文件

  5. 将6379的配置文件复制5份,实现三主三从的配置

  6. 将复制过后的配置文件中的6379全部改成对应的端口号 一键将6379全部替换“:%s/6379/63xx”

  7. 将六个redis服务都启动起来 image-20210525142226755

  8. 确保所有的node-xxxx.conf文件都自动生成

  9. 进入安装的redis中的src目录下

  10. 执行:

    1.  redis-cli --cluster create --cluster-replicas 1 xxx.xxx.xxx.xxx:port xxx.xxx.xxx.xxx:port xxx.xxx.xxx.xxx:port xxx.xxx.xxx.xxx:port xxx.xxx.xxx.xxx:port xxx.xxx.xxx.xxx:port 
      

      其中1表示以最简单的方式来搭建集群,即一个主机一个从机

  11. 连接主机 :redis-cli -c -p port 采用集群方式连接

  12. 查看集群状态 : cluster nodes

集群操作和故障恢复

[OK] All 16384 slots covered.

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

集群使用公式 CRC16(key)%16384 来计算键key属于哪一个槽,其中CRC16(key)语句用于计算键key的CRC16校验和,集群中的每个节点负责处理一部分插槽

image-20210525150601508

​ 图片中就表示了那些插槽负责哪些节点处理

redis不能用mset默认格式批量插入

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

以前默认是 : mset k1 v1 k2 v2 
但集群中要 : mset k1{id} v1 k2{id} v2 
  • 查询集群中的值

    •   cluster getkeysinslot <slot> <count> 返回count个slot槽中的键 
      
    •   cluster keyslot id :返回名为批量存入时id的值的插槽的值
      
      cluster countkeysinslot 插槽位置 :返回插槽位置的值的数量
      

故障恢复:

  • 主节点下线,从节点15秒钟超时自动升级为主节点
  • 主节点恢复后,变成从机
  • image-20210525155711615

集群的Jedis开发

因为redis是无中心化集群,无论从哪台主机写的程序,其他主机上都能读到数据

public class RedisClusterDemo{
    public static void main(String[] args){
        //创建对象
        HostAndPost hostAndPost = new HostAndPost("xxx.xxx.xxx.xxx",6379);//这里的port写哪个端口都可以,因为他是无中心集群
        JedisCluster jedisCluster = new JedisCluster(hostAndPost);
        //进行操作
        jedisCluster.set("k1","value1");
        
        String value = jedisCluster.get("k1");
        System.out.println(value);
        //关闭
        jedisCluster.close();
    }
}

缓存常见面试问题

缓存穿透

  • key对应的数据在数据源并不存在,每次针对此key的请求从缓存中获取不到,请求都会压到数据库,从而可能压垮数据库

    • 比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库
    • image-20210526164939976

    解决方案:

    1. 对空值缓存:如果一个查询返回的数据为空(不管是数据是否存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。
    2. 设置可访问的名单(白名单):使用 bitmaps 类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id作比较,如果访问id不在bitmap里面,进行拦截,不允许访问
    3. 采用布隆过滤器:(Bloom Filter)它实际上是一个很长的二级制向量(位图)和一系列随机映射函数(哈希函数),布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。将所有的可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力
    4. 进行实时监控:当redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

缓存击穿

  • key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般会从后端数据库加载数据并回设到缓存,这个时候大并发的请求会瞬间把数据库压垮

    • image-20210526171817016

    解决方案:key可能会在某些时间点被超高并发访问,是一种非常“热点”的数据,这个时候,需要考虑一个问题:缓存被“击穿”的问题

    1. 预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长

    2. 实时调整:现场监控哪些数据热门,实时调整key的过期时长

    3. 使用锁:(效率低)

      1. 就是在缓存失效的时候(判断拿出来的值为空),不是立即去加载数据库
      2. 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的setnx)去set一个mutex key
      3. 当操作返回成功时,再进行加载数据库的操作,并回设缓存,最后删除mutex key
      4. 当操作返回失败时,证明有线程再加载数据库,当前线程睡眠一段时间再重试整个get缓存的方法

      image-20210526173054276

缓存雪崩

image-20210526173232620

image-20210526173344390

大量key集中过期

解决方案:

  1. 构建多级缓存架构:nginx缓存+redis缓存+其他缓存(ehcache等)
  2. 使用锁或队列:用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行独写,从而避免失效时大量的并发请求落到底层存储系统上。不适合高并发情况。
  3. 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存
  4. 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存过期时间重复率就会降低,就很难引发集体失效时间。

分布式锁

posted @ 2021-05-13 13:50  GSCicode  阅读(49)  评论(0)    收藏  举报