Redis学习笔记

redis相关内容

  • 默认端口6379

  • 默认16个数据库,初始默认使用0号库

  • 使用select 切换数据库

  • 统一密码管理,所有库密码相同

  • dbsize:查看当前库key的数量

  • flushdb:清空当前库

  • flushall:清空全部库

  • redis是单线程 + 多路IO复用技术

    • **多路复用:**使用一个线程来检测多个文件描述符(socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞到超时,得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)
  • 串行 vs 多线程+锁(memcached) vs 单线程+多路复用(redis)

    • (与memcache三不同:支持多数据类型,支持持久化,单线程+多路复用)
    • redis6.0中提供了多线程,命令解析和io数据读写这部分采用了多线程,而命令的执行还是采用的是单线
      程,多个客户端发送来的命令会在同一个线程去执行,相当于排队执行,效率极高

数据类型

这里的数据类型指value数据类型,key类型都是字符串

redis键(key)

  • keys *:查看当前库所有的key
  • exists key:判断某个key是否存在
  • type key:查看你的key是什么类型
  • del key:删除指定的key数据
  • unlink key:根据value删除非阻塞删除,仅仅将keys从keyspace元数据中删除,真正的删除会在后续异步中操作。
  • expire key 10:为指定的key设置有效期10秒
  • ttl key:查看指定的key还有多少秒过期,-1:表示永不过期,-2:表示已过期
  • select dbindex:切换数据库【0-15】,默认为0
  • dbsize:查看当前数据库key的数量
  • flushdb:清空当前库
  • flushall:通杀全部库

redis字符串(String)

String是Redis最基本的类型 ,一个key对应一个value。是二进制安全的。意味着Redis的string可以包含任何数据(eg:jpg图片或者序列化的对象),一个Redis中字符串value最多可以是512M 。

常用命令

  • set:添加键值对
    • NX:当数据库中key不存在时,可以将key-value添加到数据库
    • XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
    • EX:key的超时秒数
    • PX:key的超时毫秒数,与EX互斥
    • value中若包含空格、特殊字符,需用双引号包裹
  • get:获取值
  • apend:追加值:将给定的值追加到原值的末尾
  • strlen:获取值的长度
  • setnx:key不存在时,设置key的值
  • incr:原子递增,将key中存储的值增1,只能对数字值操作,如果key不存在,则会新建一个,值为1
  • decr:原子递减1,将key中存储的值减1,只能对数字值操作,如果为空,新增值为-1
  • incrby/decrby:递增或递减指定的数字
  • mset:同时设置多个key-value
  • mget:获取多个key对应的值
  • msetnx:当多个key都不存在时,则设置成功 。原子性的,要么都成功,或者都失败
  • getrange:获取值的范围,类似java中的substring
  • setrange:覆盖指定位置的值
  • setex:设置键值&过期时间(秒
  • getset:以新换旧,设置新值同时返回旧值

数据结构

简单动态字符串,是可以修改的字符串,采用分配冗余空间的方式来减少内存的频繁分配 。内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次会多扩容1M的空间。

redis列表(List)

单键多值,双向链表实现。

命令

  • lpush/rpush:从左边或者右边插入一个或多个值
  • lrange:从列表左边获取指定范围内的值
  • lpop/rpop:从左边或者右边弹出多个元素
  • rpoplpush:从一个列表右边弹出一个元素放到另外一个列表中
  • lindex:获取指定索引位置的元素(从左到右)
  • llen:获得列表长度
  • linsert:在某个值的前或者后面插入一个值
  • lrem:删除指定数量的某个元素
  • lset:替换指定位置的值

数据结构

快速链表quickList

多个zipList使用双向指针串起来使用

redis集合(Set)

set是可以自动排重的,底层实际是一个value为null的hash表,收益添加,删除,查找复杂度都是O(1)

命令

  • sadd:添加一个或多个元素
  • smembers:取出所有元素
  • sismember:判断集合中是否有某个值
  • scard:返回集合中元素的个数
  • srem:删除多个元素
  • spop:随机弹出多个值
  • srandmember:随机获取多个元素,不会从集合中删除
  • smove:将某个原创从一个集合移动到另一个集合
  • sinter:取多个集合的交集
  • sinterstore:将多个集合的交集放到一个新的集合中
  • sunion:取多个集合的并集,自动去重
  • sunionstore:将多个集合的并集放到一个新的集合中
  • sdiff:取多个集合的差集
  • sdiffstore:将多个集合的差集放到一个新的集合中

数据结构

set数据结构是字典,字典是用hash表实现的。内部使用hash结构,所有的value都指向同一个内部值。

redis哈希(hash)

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

常用命令

  • hset:设置多个field的值
  • hget:获取指定filed的值
  • hgetall:返回hash表所有的域和值
  • hexists:判断给定的field是否存在,1:存在,0:不存在
  • hkeys:列出所有的filed
  • hvals:列出所有的value
  • hlen:返回filed的数量
  • hincrby:filed的值加上指定的增量
  • hsetnx:当filed不存在的时候,设置filed的值

数据结构

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

redis有序集合zset(sorted set)

redis有序集合zset是一个没有重复元素的字符串集合。有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分是可以重复的。

常用命令

  • zadd:添加元素
  • zrange:score升序,获取指定索引范围的元素
  • zrevrange:score降序,获取指定索引范围的元素
  • zrangebyscore:按照score升序,返回指定score范围内的数据
  • zrevrangebyscore:按照score降序,返回指定score范围内的数据
  • zincrby:为指定元素的score加上指定的增量
  • zrem:删除集合中多个元素
  • zremrangebyrank:根据索引范围删除元素
  • zremrangebyscore:根据score的范围删除元素
  • zcount:统计指定score范围内元素的个数
  • zrank:按照score升序,返回某个元素在集合中的排名
  • zrevrank:按照score降序,返回某个元素在集合中的排名
  • zscore:返回集合中指定元素的score

数据结构

  • hash表
    • 类似于java中的Map,key为集合中的元素,value为元素对应的score,可以用来快速定位元素定义的score,时间复杂度为O(1)
  • 跳表
    • 跳表在原来的有序链表上加上了多级索引,实现简单,插入、删除、查找的复杂度均为O(logN)
    • 有序链表基础上增加“跳跃”功能
    • 空间换时间
    • 层:就是我们假设的抽象出来的新链表。

Bitmaps:位操作字符串

可以实现对位的操作:

  • Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作,字符串中每个字符对应1个字节,也就是8位,一个字符可以存储8个bit位信息。
  • Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

常用命令

  • setbit:设置某个偏移量的值(0或1)
  • getbit:获取某个偏移位的值
  • bitcount:统计bit位都为1的数量
  • bittop:对一个多个bitmaps执行位操作

bitmaps与set比较

  • set 和 Bitmaps 存储一天活跃用户对比
    • 使用 Bitmaps 能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的。
  • set 和 Bitmaps 存储独立用户空间对比
    • 假如该网站每天的独立访问用户很少, 例如只有 10 万(大量的僵尸用户), 那么两者的对比如下表所示, 很显然, 这时候使用 Bitmaps 就不太合适了, 因为基本上大部分位都是 0。

HyperLoglog

是用来做基数统计的算法,HyperLogLog 的优点是:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

命令

  • pfadd:添加多个元素
  • pfcount:获取多个HLL合并后元素的个数
  • pfmerge:将多个HLL合并后元素放入另外一个HLL

Geographic

该类型,就是元素的 2 维坐标,在地图上就是经纬度,redis基于该类型,提供了经纬度设置、查询、范围查询、距离查询,经纬度Hash等常见操作。

命令

  • geoadd:添加多个位置的经纬度
  • geopos:获取多个位置的坐标值
  • geodist:获取两个位置的直线距离
  • georadius:以给定的经纬度为中心,找出某一半径内的元素

发布和订阅

发布和订阅

消息通信模式:

  • 发布者发布消息
  • 订阅者接收消息

redis发布和订阅

  • 客户端订阅频道:客户端A,B,C—订阅 —>channel1频道
  • 当给这个频道发布消息后,消息就会发送给订阅的客户端

发布和订阅的命令行实现

  • 订阅命令:subscribe channel1
  • 给channel1发布消息hello:publish channel1 helloworld
  • 切换到订阅窗口即可看到

发布和订阅常用命令

  • subscribe:订阅一个或者多个频道
    • 订阅msg和chat_room两个频道:subscribe msg chat_root
  • publish:发布消息到指定的频道
    • publish msg “hello”
  • psubscribe:订阅一个或多个符合给定模式的频道
    • 每个模式以 * 作为匹配符,eg:it* 匹配以it开头的频道
    • psubscribe news.* tweet.*

Jedis

maven

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.3</version>
</dependency>

api

package jedis;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author: 308158
 * @description: TODO
 * @date: Created in 2023-8-17 13:58
 */
public class JedisDemo {


    Jedis jedis;

    @Before
    public void before(){
        this.jedis = new Jedis("1.1.1.1", 6379);
    }

    @After
    public void after(){
        //关闭jedis
        this.jedis.close();
    }

    /** 测试redis是否连通 */
    @Test
    public void test1(){
        String ping = jedis.ping();
        System.out.println(ping);
    }

    /** string类型测试 */
    @Test
    public void stringTest(){
        jedis.set("site", "http://");
        System.out.println(jedis.get("site"));
        System.out.println(jedis.ttl("site"));
    }

    /** list类型测试 */
    @Test
    public void listTest(){
        jedis.rpush("courses", "java", "spring", "springmvc");
        List<String> courses = jedis.lrange("courses", 0, -1);
        for (String course : courses) {
            System.out.println(course);
        }
    }

    /** set类型 */
    @Test
    public void setTest(){
        jedis.sadd("users","tom","jack","ready");
        Set<String> users = jedis.smembers("users");
        for (String user : users) {
            System.out.println(user);
        }
    }

    /** hash类型测试 */
    @Test
    public void hashTest() {
        jedis.hset("user:1001", "id", "1001");
        jedis.hset("user:1001", "name", "张三");
        jedis.hset("user:1001", "age", "30");
        Map<String, String> userMap = jedis.hgetAll("user:1001");
        System.out.println(userMap);
    }

    /** zset类型测试 */
    @Test
    public void zsetTest() {
        jedis.zadd("languages", 100d, "java");
        jedis.zadd("languages", 95d, "c");
        jedis.zadd("languages", 70d, "php");
        Set<String> languages = jedis.zrange("languages", 0, -1);
        System.out.println(languages);
    }

    /** 订阅消息 */
    @Test
    public void subscribeTest() throws InterruptedException {
        //subscribe(消息监听器,频道列表)
        jedis.subscribe(new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
                System.out.println(channel + ":" + message);
            }
        }, "sitemsg");
        TimeUnit.HOURS.sleep(1);
    }

    /** 发布消息*/
    @Test
    public void publishTest() {
        jedis.publish("sitemsg", "hello redis");
    }

}

springboot整合redis

maven配置

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

配置信息

#redis服务器ip
spring.redis.host=192.168.1.1
#redis服务器端口
spring.redis.port=6379
#redis密码
spring.redis.password=root
#连接超时时间
spring.redis.timeout=6000
#redis默认情况下有16个分片,默认0
spring.redis.database=0

RedisTemplate工具类操作redis

package jedis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author: 308158
 * @description: TODO
 * @date: Created in 2023-8-17 14:17
 */
@RestController
@RequestMapping("/redis")
public class RedisController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/stringTest")
    public String stringTest() {
        this.redisTemplate.delete("name");
        this.redisTemplate.opsForValue().set("name", "路人");
        return this.redisTemplate.opsForValue().get("name");
    }

    @RequestMapping("/listTest")
    public List<String> listTest() {
        this.redisTemplate.delete("names");
        this.redisTemplate.opsForList().rightPushAll("names", "刘德华", "张学友", "郭富城", "黎明");
        return this.redisTemplate.opsForList().range("names", 0, -1);
    }

    @RequestMapping("setTest")
    public Set<String> setTest() {
        this.redisTemplate.delete("courses");
        this.redisTemplate.opsForSet().add("courses", "java", "spring", "springboot");
        return this.redisTemplate.opsForSet().members("courses");
    }

    @RequestMapping("hashTest")
    public Map<Object, Object> hashTest() {
        this.redisTemplate.delete("userMap");
        Map<String, String> map = new HashMap<>();
        map.put("name", "路人");
        map.put("age", "30");
        this.redisTemplate.opsForHash().putAll("userMap", map);
        return this.redisTemplate.opsForHash().entries("userMap");
    }

    @RequestMapping("zsetTest")
    public Set<String> zsetTest() {
        this.redisTemplate.delete("languages");
        this.redisTemplate.opsForZSet().add("languages", "java", 100d);
        this.redisTemplate.opsForZSet().add("languages", "c", 95d);
        this.redisTemplate.opsForZSet().add("languages", "php", 70);
        return this.redisTemplate.opsForZSet().range("languages", 0, -1);
    }
}
}

redis 事务

定义

  • redis事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 主要作用:串联多个命令防止别的命令插队。

Multi,Exec,discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,redis会将之前的命令依次执行。组队的过程中可以通过discard来放弃组队。

redis事务分2个阶段:组队阶段、执行阶段

  • 组队阶段:只是将所有命令加入命令队列
  • 执行阶段:依次执行队列中的命令,在执行这些命令的过程中,不会被其他客户端发送的请求命令插队或者打断。

相关的命令

multi:标记一个事务块的开始

事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 exec 命令原子性(atomic)地执行

exec:执行所有事务块内的命令

假如某个(或某些) key 正处于 watch 命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 exec 命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。

返回值:
事务块内所有命令的返回值,按命令执行的先后顺序排列。
当操作被打断时,返回空值 nil 。

discard:取消事务

取消事务,放弃执行事务块内的所有命令。

返回值:总是返回ok

事务的错误处理

case1:组队中命令有误,导致所有命令取消执行

组队中某个命令出现了错误报告,执行时整个队列中所有的命令都会被取消

case2:组队中没有问题,执行中部分成功部分失败

事务冲突的问题

悲观锁

每次去拿数据的时候都认为会修改,所以每次在拿数据的时候都会上锁,这样别人拿到这个数据就会block直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁

乐观锁

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在修改的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。redis就是使用这种check-and-set机制实现事务的。

watch key [key…]

在执行multi之前,先执行watch key1 [key2 …],可以监视一个或者多个key,若在事务的exec命令之前这些key对应的值被其他命令所改动了,那么事务中所有命令都将被打断,即事务所有操作将被取消执行 。

unwatch:取消监视

如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。

redis事务三特性

单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发送来的命令请求所打断

没有隔离级别的概念

队列中的命令没有提交(exec)之前,都不会实际被执行,因为事务提交前任何指令都不会被实际执行。

不能保证原子性

事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。
如果在组队阶段,有1个失败了,后面都不会成功;如果在组队阶段成功了,在执行阶段有那个命令失败就这条失败,其他的命令则正常执行,不保证都成功或都失败。

redis持久化–RDB

定义

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

备份如何执行

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

Fork

  • 作用:复制一个与当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,它是一个全新的进程,并作为原进程的子进程。
  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,linux中引入了“写时复制技术”
  • 一般情况父进程和子进程会共用一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

RDB持久化流程

bgsave命令给到父进程

  • 如果有其他子进程正在执行直接返回
  • 否则,fork一个子进程,生成rdb文件并通知父进程

配置

  • 指定备份文件的名称:redis.conf中,默认为dump.rdb
  • 指定备份文件存放的目录:redis.conf中,默认dir ./
  • 触发rdb备份
    • 自动备份,需配置备份规则:redis.conf
      • save用来配置备份的规则:格式:save 秒钟 写操作次数
    • 手动执行命令备份
      • save:只管保存,其他不管,全部阻塞,手动保存,不建议使用
      • bgsave:后台异步进行快照操作,快照同时还可以相应客户端情况,可以通过lastsave命令获取最后一次成功生成快照时间
    • flushall命令
      • 执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义
  • stop-writes-on-bgsave-error:当磁盘满时,是否关闭redis的写操作
  • rdbcompression:rdb备份是否开启压缩
  • rdbchecksum:是否检查rdb备份文件的完整性

rdb备份和恢复

  • rdb备份
    • 通过config get dir 查询rdb文件的目录
    • 然后将rdb的备份文件*.rdb文件拷贝到别的地方
  • rdb的恢复
    • 关闭redis
    • 先把备份的文件拷贝到工作目录
    • 启动redis,备份数据直接加载,数据被恢复

优劣势

优势

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

劣势

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

停止rdb

动态停止RDB:redis-cli config set save “” #save后给空值,表示禁用保存策略

redis事务三特性

单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发送来的命令请求所打断

没有隔离级别的概念

队列中的命令没有提交(exec)之前,都不会实际被执行,因为事务提交前任何指令都不会被实际执行。

不能保证原子性

事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。
如果在组队阶段,有1个失败了,后面都不会成功;如果在组队阶段成功了,在执行阶段有那个命令失败就这条失败,其他的命令则正常执行,不保证都成功或都失败。

redis持久化–RDB

定义

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

备份如何执行

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

Fork

  • 作用:复制一个与当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,它是一个全新的进程,并作为原进程的子进程。
  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,linux中引入了“写时复制技术”
  • 一般情况父进程和子进程会共用一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

RDB持久化流程

bgsave命令给到父进程

  • 如果有其他子进程正在执行直接返回
  • 否则,fork一个子进程,生成rdb文件并通知父进程

配置

  • 指定备份文件的名称:redis.conf中,默认为dump.rdb
  • 指定备份文件存放的目录:redis.conf中,默认dir ./
  • 触发rdb备份
    • 自动备份,需配置备份规则:redis.conf
      • save用来配置备份的规则:格式:save 秒钟 写操作次数
    • 手动执行命令备份
      • save:只管保存,其他不管,全部阻塞,手动保存,不建议使用
      • bgsave:后台异步进行快照操作,快照同时还可以相应客户端情况,可以通过lastsave命令获取最后一次成功生成快照时间
    • flushall命令
      • 执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义
  • stop-writes-on-bgsave-error:当磁盘满时,是否关闭redis的写操作
  • rdbcompression:rdb备份是否开启压缩
  • rdbchecksum:是否检查rdb备份文件的完整性

rdb备份和恢复

  • rdb备份
    • 通过config get dir 查询rdb文件的目录
    • 然后将rdb的备份文件*.rdb文件拷贝到别的地方
  • rdb的恢复
    • 关闭redis
    • 先把备份的文件拷贝到工作目录
    • 启动redis,备份数据直接加载,数据被恢复

优劣势

优势

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

劣势

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

停止rdb

动态停止RDB:redis-cli config set save “” #save后给空值,表示禁用保存策略

redis持久化 – AOF

定义

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

持久化流程

  • 客户端的请求写命令会被append追加到AOF缓冲区内
  • AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中
  • AOF文件大小超过重写策略或手动重写时,会对AOF文件进行重写(rewrite),压缩AOF文件容量
  • redis服务器重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的

默认不开启

  • redis.conf文件中对AOF进行配置
    • appendonly no # 是否开启AOF,yes:开启,no:不开启,默认为no
    • appendfilename “appendonly.aof” # aof文件名称,默认为appendonly.aof
    • dir ./ # aof文件所在目录,默认./,表示执行启动命令时所在的目录,比如在/opt目录中,去执行
    • redis-server /etc/redis.conf 来启动redis,那么dir此时就是/opt目录
  • aof和rdf同时开启,系统默认取AOF数据(数据不会存在丢失)

AOF 启动修复恢复

  • AOF的备份机制和性能虽然和RDB不同,但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。
  • 正常恢复
    • 修改默认的appendonly no,改为yes
    • 将有数据的aof文件复制一份保存到对应的目录(查看目录:config get dir)
    • 恢复:重启redis然后重新加载
  • 异常恢复
    • 修改默认的appendonly no,改为yes
    • 如遇到aof文件损坏,通过 /usr/local/bin/redis-check-aof --fix appendonly.aof 进行
      恢复

AOF 同步频率设置

redis.config配置AOF同步的频率

  • appendfsync always:每次写入立即同步
    • 始终同步,每次redis的写入都会立刻记入日志;性能较差但数据完整性比较好。
  • appendfsync everysec:每秒同步
    • 每秒同步,每秒记录日志一次,如果宕机,本秒数据可能丢失;更新的命令会放在内存中AOF缓冲区,每秒将缓冲区的命令追加到AOF文件
  • appendfsync no:不主动同步
    • redis不主动进行同步,把同步交给操作系统。

rewrite压缩

rewrite压缩

AOF采用文件追加方式,文件会越来越大,为了避免出现此情况,新增了重写机制。

重写机制:当AOF文件的大小超过锁审定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof触发重写

重写原理实现

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

触发机制 何时重写

bgrewriteaof:手动触发重写

从 Redis 2.4 开始, AOF 重写由 Redis 自行触发, bgrewriteaof 仅仅用于手动触发重写操作。
redis会记录上次重写的aof大小,默认配置是当aof文件大小是上次rewrite后大小的2倍且文件大于64M时触发。
重写虽然可以节约大量磁盘空间,减少恢复时间,但是每次重写还是有一定负担的,因此设置redis满足一定条件才会进行重新。

auto-aof-rewrite-percentage:设置重写基准值

设置重写的基准值,默认100,当文件达到100%时开始重写(文件是原来重写后文件的2倍时重写)。

auto-aof-rewrite-min-size:设置重写基准值

设置重写的基准值,默认64MB,AOF文件大小超过这个值开始重写。

重写流程
  • 手动执行 bgrewriteaof 命令触发重写,判断是否当前有bgfsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行
  • 主进程fork出子进程执行重写操作,保证主进程不会阻塞
  • 子进程遍历redis内存中的数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整性以及新AOF文件生成期间的新的数据修改动作不会丢失
  • 子进程写完新的AOF文件后,向主进程发送信号,父进程更新统计信息
  • 主进程把aof_rewrite_buf中的数据写入到新的AOF文件
  • 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写
no-appendfsync-on-rewrite:重写时,不会执行appendfsync操作
  • 该参数表示在正在进行AOF重写时不会将AOF缓冲区中的数据同步到旧的AOF文件磁盘,也就是说在进行AOF重写的时候,如果此时有写操作进来,此时写操作的命令会放在aof_buf缓存中(内存中),而不会将其追加到旧的AOF文件中,这么做是为了避免同时写旧的AOF文件和新的AOF文件对磁盘产生的压力。
  • 默认是ON,表示关闭,即在AOF重写时,会对AOF缓冲区中的数据做同步磁盘操作,这在很大程度上保证了数据的安全性。但在数据量很大的场景,因为两者都会消耗磁盘IO,对磁盘的影响较大,可以将其设置为“yes”减轻磁盘压力,但在极端情况下可能丢失整个AOF重写期间的数据。
  • 如果no-appendfsync-on-rewrite为yes,不写入aof文件,只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)如果no-appendfsync-on-rewrite为no,还是会把数据库往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

优劣势

优势

  • 备份机制更稳健,丢失数据概率更低
  • 可读的日志文本,通过操作AOF文件,可以处理误操作

劣势

  • 比RDB占用更多的磁盘空间
  • 恢复备份速度要慢
  • 每次读写都同步的话,有一定的性能压力
  • 存在个别bug,造成不能恢复
总结
  • AOF文件是一个只进行追加的日志文件
  • Redis可以在AOF文件体积变得过大时,自动地在后台对AOF文件进行重写
  • AOF文件有序地保存了对数据库执行的所有写入操作,这些写入操作以redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松。
  • 对于相同的数据集来说,AOF文件的体积通常要大于RDB文件的体积
  • 根据所使用的fsync策略,AOF的速度可能会慢于RDB

redis主从复制

定义

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

作用

  • 读写分离,性能扩展,降低主服务器的压力
  • 容灾,快速恢复,主机挂掉时,从机变为主机

用法

配置1主2从
  • master(主):6379
  • slave1(从):6380
  • slave2(从):6381
配置主从
  • 创建案例工作目录:master-slave
    • 执行下面命令创建 /opt/master-slave 目录,本次所有操作,均在 master-slave 目录进行。
  • 将redis.conf复制到master-slave目录
  • 创建master的配置文件:redis-6379.conf
  • 创建slave1的配置文件:redis-6380.conf
  • 创建slave2的配置文件:redis-6381.conf
  • 启动master :redis-server 命令
  • 启动slave1
  • 启动slave2
  • 查看主机的信息 :
    • 连接主机:redis-cli -h xx.xx.
    • 查看信息:info Repliacation
  • 查看slave1的信息
  • 同样查看slave2的信息
  • 验证主从同步效果
    • flushdb
    • set name ready
    • 查看从机是否把数据同步过来了
主从复制原理
  • slave启动成功连接到master后,会给master发送数据同步消息(发送sync命令)
  • master接收到slave发来的数据同步消息后,把主服务器的数据进行持久化到rdb文件,同时会收集接收到的用于修改数据的命令,master将传rdb文件发送给你slave,完成一次完全同步
  • 全量复制:而slave服务在接收到master发来的rdb文件后,将其存盘并加载到内存
  • 增量复制:master继续将收集到的修改命令依次传给slave,完成同步
  • 但是只要重新连接master,一次完全同步(全量复制)将会被自动执行
小结
  • 主redis挂掉以后情况会如何?
    • 主机挂掉后,从机会待命
  • 从挂掉后又恢复了,会继续从主同步数据么?
    • 会的,当从重启之后,会继续将中间缺失的数据同步过来。

常用的主从结构

一主二从

从机可以采用命令的方式配置

  • 创建案例工作目录:master-slave
  • 将redis.conf复制到master-slave
  • 创建master的配置文件:redis-6379.conf
  • 创建slave1的配置文件:redis-6380.conf
  • 创建slave2的配置文件:redis-6381.conf
  • 启动master ,slave1 ,slave2 :redis-server 命令
  • 分别登陆3台机器,查看各自主从信息 :info replication
  • 配置slave1为master的从库
    • 连接到slave1:redis-cli
    • 设置master密码:config set
    • 指定slave1作为master从机:slaveof
  • 配置slave2为master的从库
  • 再来看看master的主从信息

注意:通过 slaveof 命令指定主从的方式,slave重启之后主从配置会失效,所以,重启后需要在slave上重新通过 slaveof 命令进行设置。中途通过 slaveof 变更转向,本地的数据会被清除,会从新的master重新同步数据。

薪火相传

若master下面挂很多slave,master会有压力,实际上slave下面也可以挂slave,配置和上面的类似。

反客为主

当master挂掉之后,可以从slave中选择一个作为主机。

  • 执行命令:slaveof no one
  • slave1就变成主机了,然后再去其他slave上面执行 slaveof 命令将其挂在slave1上。

缺点:需要手动去执行命令去操作,不是太方便。
其他方案:哨兵模式,主挂掉之后,自动从slave中选举一个作为主机,自动实现故障转移。

哨兵模式

定义

能够自动监控master是否发生故障,如果故障了会根据投票数从slave中挑选一个作为master,其他的slave会自动转向同步新的master,实现故障自动转义。

原理

  • sentinel会按照指定的频率给master发送ping请求,看看master是否还活着
  • 若master在指定时间内未正常响应sentinel发送的ping请求,sentinel则认为master挂掉了
  • 存在误判:可能master并没有挂,只是sentinel和master之间的网络不通导致,导致ping失败。
  • 为了避免误判,通常会启动多个sentinel,一般是奇数个,比如3个,那么可以指定当有多个sentinel都觉得master挂掉了,此时才断定master真的挂掉了,通常这个值设置为sentinel的一半,比如sentinel的数量是3个,那么这个量就可以设置为2个。
  • 当多个sentinel经过判定,断定master确实挂掉了
  • sentinel会进行故障转移:会从slave中投票选出一个服务器,将其升级为新的主服务器, 并让失效主服务器的其他从服务器slaveof指向新的主服务器;
  • 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

用法

需求:配置1主2从3个哨兵

最少有2个哨兵认为主的挂掉了,才进行故障转移。

  • master:6379
  • slave1:6380
  • slave2:6381
  • sentinel1:26379
  • sentinel2:26380
  • sentinel3:26381

步骤:

  • 创建案例工作目录 sentinel
  • 将redis.conf复制到sentinel目录
  • 创建master的配置文件:redis-6379.conf
  • 创建slave1的配置文件:redis-6380.conf
  • 创建slave2的配置文件:redis-6381.conf
  • 启动master、slave1、slave2
  • 配置slave1为master的从库
  • 配置slave2为master的从库
  • 验证主从复制是否正常
  • 创建sentinel1的配置文件:sentinel-26379.conf
  • 创建sentinel2的配置文件:sentinel-26380.conf
  • 创建sentinel3的配置文件:sentinel-26381.conf
  • 启动3个sentinel
  • 分别查看3个sentinel的信息 :info sentinel
  • 验证故障自动转移是否成功
    • step1:在master中执行下面命令,停止master :shutdown
    • step2:等待2分钟,等待完成故障转移
      • sentinel中的配置:down-after-milliseconds:60 :表示判断主机下线时间是60秒,所以等2分钟,让系统先自动完成故障转移
    • step3:查看slave1的主从信息 info replication
    • step4:查看slave2的主从信息
    • step5:验证下slave1和slave2是否同步
  • 恢复旧的master:当旧的master恢复之后,会自动挂在新的master下面
    • step1:执行命令,启动旧的master :redis-server
    • step2:执行命令,连接旧的master
    • step3:执行命令,查看其主从信息

springboot整合sentinel模式

配置

# redis sentinel主服务名称,这个可不是随便写的哦,来源于:sentinel配置文件中sentinel monitor后面跟的那个名称
spring.redis.sentinel.master=mymaster
# sentinel节点列表(host:port),多个之间用逗号隔开
spring.redis.sentinel.nodes=192.168.200.129:26379,192.168.200.129:26380,192.168.200.129:26381
# sentinel密码
#spring.redis.sentinel.password=
# 连接超时时间(毫秒)
spring.redis.timeout=60000
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
spring.redis.database=0
@RequestMapping(value = "/info", produces = MediaType.TEXT_PLAIN_VALUE)
public String info() {
    Object obj = this.redisTemplate.execute(new RedisCallback() {
        @Override
        public Object doInRedis(RedisConnection connection) throws DataAccessException {
            return connection.execute("info");
        }
    });
    return obj.toString();
}
 
  

redis集群

存在问题

  • 单台redis容量限制,如何进行扩容?继续加内存、加硬件么?
  • 单台redis并发写量太大有性能瓶颈,如何解决?
  • redis3.0中提供了集群可以解决这些问题。

集群定义

redis集群是对redis的水平扩容,即启动N个redis节点,将整个数据分布存储在这个N个节点中,每个节点存储总数据的1/N。
例如:由3台master和3台slave组成的redis集群,每台master承接客户端三分之一请求和写入的数据,当master挂掉后,slave会自动替代master,做到高可用。

配置

需求:配置3主3从集群

master1:6379 master2:6380 master3:6381

slave1:6389 slave2:6390 slave3:6391

  • 创建案例工作目录:cluster
  • 将redis.conf复制到cluster目录
  • 创建master1的配置文件:redis-6379.conf
  • 创建master2的配置文件:redis-6380.conf
  • 创建master3的配置文件:redis-6381.conf
  • 创建slave1的配置文件:redis-6389.conf
  • 创建slave2的配置文件:redis-6390.conf
  • 创建slave3的配置文件:redis-6391.conf
  • 启动master、slave1、slave2
  • 查看6个redis的启动情况 ps -ef | grep redis
  • 确保node-xxxx.conf文件已正常生成
  • 将6个节点合成一个集群
    • /opt/redis-6.2.1/src/redis-cli --cluster create --cluster-replicas 1
      192.168.200.129:6379 192.168.200.129:6380 192.168.200.129:6381
      192.168.200.129:6389 192.168.200.129:6390 192.168.200.129:6391
    • 合体的命令后面会跟上所有节点的ip:port列表,多个之间用空格隔开,注意ip不要写127.0.0.1,要写真实ip
    • –cluster-replicas 1:表示采用最简单的方式配置集群,即每个master配1个slave,6个节点就形成了3主3从
    • 执行期间会让确定是否同样这样的分配方式,输入:yes,然后等几秒,集群合体成功
  • 连接集群节点,查看集群信息:cluster nodes
  • 验证集群数据的读写操作
    • 连接 6379 这个节点,然后执行一个set操作
    • [root@hspEdu01 cluster]# redis-cli -c -h 192.168.200.129 -p 6379
      192.168.200.129:6379> set name ready
      -> Redirected to slot [5798] located at 192.168.200.129:6380
      OK
      192.168.200.129:6380>
    • 明明在 6379 上操作的,但是请求被转发到了6380这个节点去处理了涉及slot了

redis集群如何分配这6个节点

  • 一个集群至少有3个主节点,因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。
  • 选项–cluster-replicas 1表示希望为集群中的每个主节点创建一个从节点。
    分配原则尽量保证每个主库运行在不同的ip,每个主库和从库不在一个ip上,这样才能做到高可用。

slots(槽)

Redis集群内部划分了16384个slots(插槽),合并的时候,会将每个slots映射到一个master上面,比如上面3个master和slots的关系如下

  • master1:0-5460,插槽位置从0开始,0表示第1个插槽
  • master2:5460-10922
  • master3:10923-16383
  • slave1,slave2,slave3:从节点没有槽位,slave是用来对master做替补的

数据库中的每个key都属于16384个slots中的其中1个,当通过key读写数据的时候,redis需要先根据key计算出key对应的slots,然后根据slots和master的映射关系找到对应的redis节点,key对应的数据就在这个节点上面。
集群中使用公式 CRC16(key)%16384 计算key属于哪个槽

在集群中录入值

在 redis-cli 每次录入、查询键值,redis都会计算key对应的插槽,如果不是当前redis节点的插槽,redis会报错,并告知应前往的redis实例地址和端口。

连接6379这个实例来操作k1,这个节点发现k1的槽位在6381上面,返回了错误信息。

error:moved slot ip: port

使用redis-cli客户端提供了-c参数可以解决这个问题,表示以集群方式执行,执行命令的时候当前节点处理不了的时候,会自动将请求重定向到目标节点,效果如下,被重定向到6381了

redis-cli -c -h xxxxxx 6379

redirected to slot [12706] located at ip:port(6381)

同样,执行get也会被重定向

slot相关的一些命令

  • cluster keyslot :计算key对应的slot
  • cluster coutkeysinslot :获取slot槽位中key的个数
  • cluster getkeysinslot 返回count个slot槽中的键

故障恢复

如果主节点下线,从节点是否能够提升为主节点?注意:要等15秒

如果某一段插槽的主从都宕机了,redis服务是否还能继续?

要看 cluster-require-full-coverage 参数的值了

  • yes(默认值):整个集群都都无法提供服务了
  • no:宕机的这部分槽位数据全部不能使用,其他槽位正常

springboot 整合redis集群

配置

# 集群节点(host:port),多个之间用逗号隔开
spring.redis.cluster.nodes=192.168.200.129:6379,192.168.200.129:6380,192.168.200
.129:6381,192.168.200.129:6389,192.168.200.129:6390,192.168.200.129:6391
# 连接超时时间(毫秒)
spring.redis.timeout=60000

应用问题解决

缓存穿透

问题描述

当系统中引入redis缓存后,一个请求进来后,会先从redis缓存中查询,缓存有就直接返回,缓存中没有就去db中查询,db中如果有就会将其丢到缓存中,但是有些key对应更多数据在db中并不存在,每次针对此次key的请求从缓存中取不到,请求都会压到db,从而可能压垮db。
比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用大量此类攻击可能压垮数据库

解决方案

对空值缓存

如果一个查询返回的数据为空(不管数据库是否存在),我们仍然把这个结果(null)进行缓存,给其设置一个很短的过期时间,最长不超过五分钟

设置可访问的名单(白名单)

使用redis中的bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次和bitmap里面的id进行比较,如果访问的id不在bitmaps里面,则进行拦截,不允许访问

采用布隆过滤器
  • 一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)
  • 可以用于检测一个元素是否在一个集合中,
  • 优点:空间效率和查询的时间都远超一般算法
  • 缺点:有一定的误识别率和删除困难
  • 将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力
进行实时监控

实时监控,可以设置黑名单限制对其提供服务

缓存击穿

问题描述

redis中某个热点key(访问量很高的key)过期,此时大量请求同时过来,发现缓存中没有命中,这些请求都打到db上了,导致db压力瞬时大增,可能会打垮db,这种情况成为缓存击穿。
现象:

  • 数据库访问压力瞬间增大
  • redis里面没有出现大量的key过期
  • redis正常运行

解决方案

预先设置热门数据,适时调整过期时间

在redis高峰之前,把热门数据提前存入到redis里面,对缓存中这些热门数据进行监控,实时调整过期时间。

使用锁

缓存中拿不到数据的时候,此时不是立即去db中查询,而是去获取分布式锁(例如redis中的setnx)拿到锁再去db中load数据,没有拿到锁的线程休眠一段时间再重试整个获取数据的方法。

缓存雪崩

问题描述

key对应的数据存在,但是极短时间内有大量的key集中过期,此时若有大量的并发请求过来,发现缓存没有数据,大量的请求就会落到db上去加载数据,会将db击垮,导致服务奔溃。
缓存雪崩与缓存击穿的区别在于:前者是大量的key集中过期,而后者是某个热点key过期。

解决方案

构建多级缓存

nginx缓存+redis缓存+其他缓存

使用锁或队列

用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,不适用高并发情况

监控缓存过期,提前更新

监控缓存,发下缓存快过期了,提前对缓存进行更新。

将缓存失效时间分散开

在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样缓存的过期时间重复率就会降低,就很难引发集体失效的事件。

分布式锁

问题描述

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

分布式锁主流的实现方案

  1. 基于数据库实现分布式锁
  2. 基于缓存(redis等)
  3. 基于zookeeper
    每一种分布式锁解决方案都有各自的优缺点
  4. 性能:redis最高
  5. 可靠性:zookeeper最高

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

命令:
set key value NX PX 有效期(毫秒)

表示:
当key不存在的时候,设置其值为value,且同时设置其有效期
eg:

set sku:1:info "ok" NX PX 10000

**表示:**当 sku:1:info 不存在的时候,设置值为ok,且有效期为1万毫秒

上锁过程

执行 set key value NX PX 有效期(毫秒) 命令,返回ok表示执行成功,则获取锁成功,多个客户端并发执行此命令的时候,redis可确保只有一个可以执行成功

  • 客户端发起命令
  • 没有返回ok,上锁失败会休眠,重试
  • 返回ok,表示上锁成功,执行业务,del释放锁,结束
为什么设置过期时间

客户端获取锁后,由于系统问题,如系统宕机了,会导致锁无法释放,其他客户端就无法或锁了,所以需要给锁指定一个使用期限。

设置的有效期太短了怎么办

客户端需要实现续命的功能

解决锁误删的问题

误删:自己把别人持有的锁给删掉了

例如
线程A获取锁的时候,设置的有效期是10秒,但是执行业务的时候,A程序突然卡主了超过了10秒,此时这个锁就可能被其他线程拿到,比如被线程B拿到了,然后A从卡顿中恢复了,继续执行业务,业务执行完毕之后,去执行了释放锁的操作,此时A会执行del命令,此时就出现了锁的误删,导致的结果就是把B持有的锁给释放了,然后其他线程又会获取这个锁,挺严重的。
解决:
获取锁的之前,生成一个全局唯一id,将这个id也丢到key对应的value中,释放锁之前,从redis中将这个id拿出来和本地的比较一下,看看是不是自己的id,如果是的再执行del释放锁的操作。

还是存在误删的可能(原子操作问题)

del之前,会先从redis中读取id,然后和本地id对比一下,如果一致,则执行删除,伪代码如下

1.判断redis.get("key").id==本地id是否相当,如果是则执行2
2.del key

此时如果执行step2的时候系统卡主了,比如卡主了10秒,然后redis才收到,这个期间锁可能又被其他线程获取了,此时又发生了误删的操作。

根本原因是:判断和删除这2个步骤对redis来说不是原子操作导致的。
需要使用Lua脚本来解决

终极方案:Lua脚本来释放锁

将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数,提升性能。
Lua脚本类似于redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务的操作。
redis的LUA脚本功能,只能在redis2.6以上版本才能使用

@Autowired
private RedisTemplate<String, String> redisTemplate;

public String lock(){
	String lockKey = "k1";
	String uuid = UUID.randomUUID().toString();
	//获取锁,有效期10秒
	if (redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 10, TimeUnit.SECONDS)){
		//业务处理
		//使用lua脚本释放锁
		String script = "if redis.call('get',KEY[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
		DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
		redisScript.setScriptText(script);
		redisScript.setResultType(Long.class);
		Long result = redisTemplate.execute(redisScript, Arrays.asList(lockKey), uuid);
		System.out.println(result);
		return "获取锁成功";
	}else{
		return "加锁失败";
	}
}
分布式锁总结

为了确保分布式锁可用,至少需要确保分布式锁的实现同时满足以下四个条件

  • 互斥性,在任意时刻只能有一个客户端能够持有锁
  • 不发生死锁,即使有一个客户端在持有锁期间崩溃而没有释放锁,也能够保证后续其他客户端能够加锁
  • 加锁和解锁必须是同一个客户端,客户端不能把别人的锁给解了
  • 加锁和解锁必须有原子性

你可能感兴趣的:(redis,redis,学习,笔记)