redis【持久化 & 事务 & 锁】

本贴最后更新于 1333 天前,其中的信息可能已经时移世改

Redis 容器配置 redis.conf

  • redis 容器里边的配置文件是需要在创建容器时映射进来的
停止容器:docker container stop myredis
删除容器:docker container rm myredis
  • 重新开始创建容器【直接在外部配置软连接到容器内部】
1. 创建docker统一的外部配置文件

mkdir -p docker/redis/{conf,data}

2. 在conf目录创建redis.conf的配置文件

touch /docker/redis/conf/redis.conf

3. redis.conf文件的内容需要自行去下载,网上很多

4. 创建启动容器,加载配置文件并持久化数据

docker run -d --privileged=true -p 6379:6379 -v /docker/redis/conf/redis.conf:/etc/redis/redis.conf -v /docker/redis/data:/data --name myredis redis redis-server /etc/redis/redis.conf --appendonly yes

1、简介

什么是持久化?

利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化。

为什么要持久化

防止数据的意外丢失,确保数据安全性

持久化过程保存什么

  • 将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据
  • 将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂,关注点在数据的操作过程

RDB

RDB 启动方式——save

  • 命令

save

  • 作用

手动执行一次保存操作

RDB 配置相关命令

  • dbfilename dump.rdb
    • 说明:设置本地数据库文件名,默认值为 dump.rdb
    • 经验:通常设置为 dump-端口号.rdb
  • dir
    • 说明:设置存储.rdb 文件的路径
    • 经验:通常设置成存储空间较大的目录中,目录名称 data
  • rdbcompression yes
    • 说明:设置存储至本地数据库时是否压缩数据,默认为 yes,采用 LZF 压缩
    • 经验:通常默认为开启状态,如果设置为 no,可以节省 CPU 运行时间,但会使存储的文件变大(巨大)
  • rdbchecksum yes
    • 说明:设置是否进行 RDB 文件格式校验,该校验过程在写文件和读文件过程均进行
    • 经验:通常默认为开启状态,如果设置为 no,可以节约读写性过程约 10% 时间消耗,但是存储一定的数据损坏风险

RDB 启动方式——save 指令工作原理

注意save 指令的执行会阻塞当前 Redis 服务器,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用

RDB 启动方式——bgsave

  • 命令

bgsave

  • 作用

手动启动后台保存操作,但不是立即执行

RDB 启动方式 —— bgsave 指令工作原理

注意bgsave 命令是针对 save 阻塞问题做的优化。Redis 内部所有涉及到 RDB 操作都采用 bgsave 的方式,save 命令可以放弃使用,推荐使用 bgsave

bgsave 的保存操作可以通过 redis 的日志查看

docker logs myredis

RDB 启动方式 ——save 配置

  • 配置
save second changes
  • 作用

满足限定时间范围内 key 的变化数量达到指定数量即进行持久化

  • 参数

second:监控时间范围

changes:监控 key 的变化量

  • 配置位置

conf 文件中进行配置

RDB 启动方式 ——save 配置原理

注意

  • save 配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的
  • save 配置中对于 second 与 changes 设置通常具有互补对应关系(一个大一个小),尽量不要设置成包含性关系
  • save 配置启动后执行的是 bgsave 操作

RDB 启动方式对比

RDB 优缺点

  • 优点
    • RDB 是一个紧凑压缩的二进制文件,存储效率较高
    • RDB 内部存储的是 redis 在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景
    • RDB 恢复数据的速度要比 AOF 很多
    • 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复
  • 缺点
    • RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据
    • bgsave 指令每次运行要执行 fork 操作创建子进程,要牺牲掉一些性能
    • Redis 的众多版本中未进行 RDB 文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象

AOF

AOF 概念

  • AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中命令,以达到恢复数据的目的。与 RDB 相比可以简单描述为改记录数据为记录数据产生的过程
  • AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式

AOF 写数据过程

AOF 写数据三种策略(appendfsync)

  • always
    • 每次写入操作均同步到 AOF 文件中,数据零误差,性能较低,不建议使用
  • everysec
    • 每秒将缓冲区中的指令同步到 AOF 文件中,数据准确性较高,性能较高建议使用,也是默认配置
    • 在系统突然宕机的情况下丢失 1 秒内的数据
  • no
    • 由操作系统控制每次同步到 AOF 文件的周期,整体过程不可控

AOF 功能开启

  • 配置

appendonly yes|no

作用

是否开启AOF持久化功能,

默认为不开启状

  • 配置

appendfsync always|everysec|no

  • 作用
    • AOF 写数据策略

AOF 重写

规则
  • 进程内已超时的数据不再写入文件
  • 忽略无效指令,重写时使用进程内数据直接生成,这样新的 AOF 文件只保留最终数据的写入命令
    • 如 del key1、 hdel key2、srem key3、set key4 111、set key4 222 等
  • 对同一数据的多条写命令合并为一条命令
    • 如 lpush list1 a、lpush list1 b、 lpush list1 c 可以转化为:lpush list1 a b c
    • 为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等类型,每条指令最多写入 64 个元素
如何使用
  • 手动重写
    bgrewriteaofCopy
    
  • 自动重写
    auto-aof-rewrite-min-size size 
    auto-aof-rewrite-percentage percentage
    
工作原理

AOF 自动重写
  • 自动重写触发条件设置
    //触发重写的最小大小
    auto-aof-rewrite-min-size size 
    //触发重写须达到的最小百分比
    auto-aof-rewrite-percentage percentCopy
    
  • 自动重写触发比对参数( 运行指令 info Persistence 获取具体信息 )
    //当前.aof的文件大小
    aof_current_size 
    //基础文件大小
    aof_base_size
    

自动重写触发条件

工作原理

缓冲策略

AOF 缓冲区同步文件策略,由参数 appendfsync 控制

  • write 操作会触发延迟写(delayed write)机制,Linux 在内核提供页缓冲区用 来提高硬盘 IO 性能。write 操作在写入系统缓冲区后直接返回。同步硬盘操作依 赖于系统调度机制,列如:缓冲区页空间写满或达到特定时间周期。同步文件之 前,如果此时系统故障宕机,缓冲区内数据将丢失。
  • fsync 针对单个文件操作(比如 AOF 文件),做强制硬盘同步,fsync 将阻塞知道 写入硬盘完成后返回,保证了数据持久化

4、RDB VS AOF

RDB 与 AOF 的选择之惑
  • 对数据非常敏感,建议使用默认的 AOF 持久化方案
    • AOF 持久化策略使用 everysecond,每秒钟 fsync 一次。该策略 redis 仍可以保持很好的处理性能,当出现问题时,最多丢失 0-1 秒内的数据。
    • 注意:由于 AOF 文件存储体积较大,且恢复速度较慢
  • 数据呈现阶段有效性,建议使用 RDB 持久化方案
    • 数据可以良好的做到阶段内无丢失(该阶段是开发者或运维人员手工维护的),且恢复速度较快,阶段 点数据恢复通常采用 RDB 方案
    • 注意:利用 RDB 实现紧凑的数据持久化会使 Redis 降的很低
  • 综合比对
    • RDB 与 AOF 的选择实际上是在做一种权衡,每种都有利有弊
    • 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF
    • 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB
    • 灾难恢复选用 RDB
    • 双保险策略,同时开启 RDB 和 AOF,重启后,Redis 优先使用 AOF 来恢复数据,降低丢失数据

Redis 事务

Redis 事务的定义

redis 事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列)。当执行时,一次性按照添加顺序依次执行,中间不会被打断或者干扰

事务的基本操作

  • 开启事务

    multiCopy
    
    • 作用
      • 作设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中
  • 取消事务

    discardCopy
    
    • 作用
      • 终止当前事务的定义,发生在 multi 之后,exec 之前
  • 执行事务

    execCopy
    
    • 作用
      • 设定事务的结束位置,同时执行事务。与 multi 成对出现,成对使用

3、事务操作的基本流程

4、事务操作的注意事项

定义事务的过程中,命令格式输入错误怎么办?

  • 语法错误
    • 指命令书写格式有误 例如执行了一条不存在的指令
  • 处理结果
    • 如果定义的事务中所包含的命令存在语法错误,整体事务中所有命令均不会执行。包括那些语法正确的命令

定义事务的过程中,命令执行出现错误怎么办?

  • 运行错误
    • 指命令格式正确,但是无法正确的执行。例如对 list 进行 incr 操作
  • 处理结果
    • 能够正确运行的命令会执行,运行错误的命令不会被执行

注意:已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现回滚。

5、基于特定条件的事务执行

  • 对 key 添加监视锁,在执行 exec 前如果 key 发生了变化,终止事务执行
    watch key1, key2....Copy
    
  • 取消对所有 key 的监视
    unwatchCopy
    

分布式锁

  • 使用 setnx 设置一个公共锁

    //上锁
    setnx lock-key value
    //释放锁
    del lock-keyCopy
    
    • 利用 setnx 命令的返回值特征,有值(被上锁了)则返回设置失败,无值(没被上锁)则返回设置成功
    • 操作完毕通过 del 操作释放锁

注意:上述解决方案是一种设计概念,依赖规范保障,具有风险性

分布式锁加强

  • 使用 expire 为锁 key 添加时间限定,到时不释放,放弃锁
    expire lock-key seconds
    pexpire lock-key millisecondsCopy
    
  • 由于操作通常都是微秒或毫秒级,因此该锁定时间不宜设置过大。具体时间需要业务测试后确认。
    • 例如:持有锁的操作最长执行时间 127ms,最短执行时间 7ms。
    • 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时
    • 锁时间设定推荐:最大耗时 120%+ 平均网络延迟 110%
    • 如果业务最大耗时 << 网络平均延迟,通常为 2 个数量级,取其中单个耗时较长即可

栗子:


/**
 * @author hax redis锁
 * Created by Administrator on 2020/9/4.
 */
@Component
public class RedisLockHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockHandler.class);

    private static final int DEFAULT_SINGLE_EXPIRE_TIME = 3;

    @Autowired
    JedisClientPools jedisClientPool;

    /**
     * 获取锁  如果锁可用   立即返回true,  否则返回false
     *
     * @param billIdentify
     * @return
     */
    public boolean tryLock(TSuperclass billIdentify) {

        TimeUnit timeUnit = TimeUnit.SECONDS;
        //设置30秒的时间进行过滤操作
        return tryLock(billIdentify, 20, timeUnit);
    }

    public void lock(TSuperclass billIdentify) {
        this.voidLock(billIdentify);
    }


    /**
     * 锁在给定的等待时间内空闲,则获取锁成功 返回true, 否则返回false
     *
     * @param billIdentify
     * @param timeout
     * @param unit
     * @return
     */
    public boolean tryLock(TSuperclass billIdentify, long timeout, TimeUnit unit) {
        String $_lockKey = (String) billIdentify.getTSuperclassKey();
        try {
            String $_lockValue = StringUtils.uuid();
            long nano = System.nanoTime();
            do {
                LOGGER.info("【获取/try】lock key: " + $_lockKey);
                Long i = jedisClientPool.setnx($_lockKey, $_lockValue);
                if (i == 1) {
                    jedisClientPool.expire($_lockKey, DEFAULT_SINGLE_EXPIRE_TIME);
                    LOGGER.info("【设置/get】 lock, key: " + $_lockKey + " , expire in " + DEFAULT_SINGLE_EXPIRE_TIME + " seconds.");
                    return Boolean.TRUE;
                } else { // 存在锁
                    if (LOGGER.isDebugEnabled()) {
                        String desc = jedisClientPool.get($_lockKey);
                        LOGGER.info("【已存在/aleary】key: " + $_lockKey + " locked by another business:" + desc);
                    }
                }
                if (timeout == 0) {
                    break;
                }
                Thread.sleep(300);
            } while ((System.nanoTime() - nano) < unit.toNanos(timeout));
            return Boolean.FALSE;
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);
            returnBrokenResource(jedisClientPool.getJedis());
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            returnResource(jedisClientPool.getJedis());
        }
        return Boolean.FALSE;
    }

    /**
     * 如果锁空闲立即返回   获取失败 一直等待
     *
     * @param billIdentify
     */
    public void voidLock(TSuperclass billIdentify) {
        String key = (String) billIdentify.getTSuperclassKey();
        try {
            do {
                LOGGER.info("lock key: " + key);
                Long i = jedisClientPool.setnx(key, key);
                if (i == 1) {
                    jedisClientPool.expire(key, DEFAULT_SINGLE_EXPIRE_TIME);
                    LOGGER.info("get lock, key: " + key + " , expire in " + DEFAULT_SINGLE_EXPIRE_TIME + " seconds.");
                    return;
                } else {
                    if (LOGGER.isDebugEnabled()) {
                        String desc = jedisClientPool.get(key);
                        LOGGER.info("key: " + key + " locked by another business:" + desc);
                    }
                }
                Thread.sleep(300);
            } while (true);
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);
            returnBrokenResource(jedisClientPool.getJedis());
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            returnResource(jedisClientPool.getJedis());
        }
    }

    /**
     * 释放锁
     *
     * @param billIdentify
     */
    public void unLock(TSuperclass billIdentify) {
        List<TSuperclass> list = new ArrayList<TSuperclass>();
        list.add(billIdentify);
        unLock(list);
    }

    /**
     * 获取所有的锁数据
     *
     * @param ids
     * @return
     */
    public List<TSuperclass> queryLocks(List<String> ids) {
        List<TSuperclass> list = new ArrayList<>();
        ids.forEach(id -> {
            list.add(TSuperclass.getVoucher(id));
        });
        return list;
    }

    /**
     * 一键释放锁
     *
     * @param ids
     * @return
     */
    public void unLocks(List<String> ids) {
        List<TSuperclass> list = this.queryLocks(ids);
        unLock(list);
    }

    /**
     * 批量释放锁
     *
     * @param billIdentifyList
     */
    public void unLock(List<TSuperclass> billIdentifyList) {
        List<String> keys = new CopyOnWriteArrayList<String>();
        for (TSuperclass identify : billIdentifyList) {
            String key = (String) identify.getTSuperclassKey();
            keys.add(key);
        }
        try {
            jedisClientPool.delbath(keys.toArray(new String[0]));
            LOGGER.info("【删除/delete】lock, keys :" + keys);
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);
            returnBrokenResource(jedisClientPool.getJedis());
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            returnResource(jedisClientPool.getJedis());
        }
    }

    /**
     * 销毁连接
     *
     * @param jedis
     */
    private void returnBrokenResource(Jedis jedis) {
        if (jedis == null) {
            return;
        }
        try {
            //容错
            jedisClientPool.getJedisPool().returnBrokenResource(jedis);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * @param jedis
     */
    private void returnResource(Jedis jedis) {
        if (jedis == null) {
            return;
        }
        try {
            jedisClientPool.getJedisPool().returnResource(jedis);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

}
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    286 引用 • 248 回帖 • 62 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...