Redis
redis安装
下载地址
http://redis.io/download
安装步骤
# 安装gcc
yum install gcc
# 把下载好的redis‐5.0.3.tar.gz放在/usr/local文件夹下,并解压
wget http://download.redis.io/releases/redis‐x.x.x.tar.gz
tar xzf redis‐x.x.x.tar.gz
cd redis‐x.x.x
# 进入到解压好的redis‐5.0.3目录下,进行编译与安装
make
# 修改配置
daemonize yes #后台启动
protected‐mode no #关闭保护模式,开启的话,只有本机才可以访问redis
# 需要注释掉bind
# bind 127.0.0.1
#(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)
# 启动服务
src/redis‐server redis.conf
# 验证启动是否成功
ps ‐ef | grep redis
# 进入redis客户端
src/redis‐cli
# 退出客户端
quit
# 退出redis服务:
(1)pkill redis‐server
(2)kill 进程号
(3)src/redis‐cli shutdown
Redis的单线程和高性能
redis是否是单线程
Redis 的单线程主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
redis高效的原因
它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。
redis的IO多路复用
redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。
Redis持久化
RDB快照(snapshot)
在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。
关闭RDB只需要将redis.conf中所有的save保存策略注释掉即可。
# save 60 10000
# save 900 1
# save 300 10
还可以手动执行命令生成RDB快照,进入redis客户端执行命令save
或bgsave
可以生成dump.rdb文件,每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。
bgsave的写时复制(COW)机制
Redis 借助操作系统提供的写时复制技术(Copy-On-Write, COW),在生成快照的同时,依然可以正常处理写命令。简单来说,bgsave
子进程是由主线程 fork
生成的,可以共享主线程的所有内存数据。bgsave
子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave
子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave
子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
AOF(append-only file)
快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘。
可通过修改redis.conf配置文件实现
# appendonly yes
# 打开 默认关闭
从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。这样的话, 当 Redis 重新启动时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。你可以配置 Redis 多久才将数据 fsync 到磁盘一次。
有三个选项:
1 appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
2 appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
3 appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。
推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。这是一种resp协议格式数据,星号后面的数字代表命令有多少个参数,$号后面的数字代表这个参数有几个字符。
注意:如果执行带过期时间的set命令,aof文件里记录的是并不是执行的原始命令,而是记录key过期的时间戳。
比如执行
> set guan yu ex 1000
对应aof文件里记录如下
AOF重写
AOF文件里可能有太多没用指令,所以AOF会定期根据内存的最新数据生成aof文件。
如下两个配置可以控制AOF自动重写频率
# auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就
# auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写
当然AOF还可以手动重写,进入redis客户端执行命令bgrewriteaof重写AOF
注意,AOF重写redis会fork出一个子进程去做(与bgsave命令类似),不会对redis正常命令处理有太多影响
RDB 和 AOF
RDB | AOF | |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
启动优先级 | 低 | 高 |
恢复速度 | 快 | 慢 |
数据安全性 | 容易丢数据 | 根据策略决定 |
生产环境可以都启用,redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据,因为aof一般来说数据更全一点。
Redis 4.0 混合持久化
重启 Redis 时,我们很少使用 RDB来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。
通过如下配置可以开启混合持久化(必须先开启aof):
# aof‐use‐rdb‐preamble yes
如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。
于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的AOF 全量文件重放,因此重启效率大幅得到提升。混合持久化AOF文件结构如下
Redis数据备份策略:
- 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份
- 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
- 每次copy备份的时候,都把太旧的备份给删了
- 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏
Redis-Lock
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lock:product_101";
/*String clientId = UUID.randomUUID().toString();
//jedis.setnx(k,v)
//setnx key vale 当且仅当key值不存在生效,若key已存在 setnx不做任何动作 保证多个线程操作时的数据不会被覆写
// 为保证出现宕机情况 加超时时间 但当超过超时时间之后,其他线程可以获取锁成功 出现超卖情况 所以在finally中进行比较 clientId
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
if (!result) {
return "error_code";
}*/
//获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
//加分布式锁 == .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
redissonLock.lock();
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
System.out.println(stock);
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
// 比较clientId 相等释放锁
/*if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
// 但是 当执行到判断方法时 发生卡顿等多种情况 导致超出超时时间未能执行释放锁操作,其他线程又获取到锁 会导致其他线程获取到的锁立刻被释放
stringRedisTemplate.delete(lockKey);
}*/
//解锁
redissonLock.unlock();
}
return "end";
}
锁续命
在主线程中加入一个分线程定时任务,每隔一段时间判断一下主线程是否还在加锁,如还在加锁,代表任务还没结束,将主线程超时时间重新设置为初始值
// 加锁方法
public void lock() {
try {
this.lockInterruptibly();
} catch (InterruptedException var2) {
Thread.currentThread().interrupt();
}
}
// lock调用
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 尝试加锁
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
// 如果ttl != null 即再来一个线程 ttl==当前线程剩余超时时间
if (ttl != null) {
// 没有抢到的线程订阅一个channel this. Subscribe() 监听这个channel
// this. Subscribe=()方法
// protected RFuture<RedissonLockEntry> subscribe(long threadId) {
// return PUBSUB.subscribe(this.getEntryName(), this.getChannelName(), this.commandExecutor.getConnectionManager().getSubscribeService());
// }
// this.getChannelName() 方法
// String getChannelName() {
// return prefixName("redisson_lock__channel", this.getName());
// }
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.commandExecutor.syncSubscription(future);
try {
while(true) {
// 再次尝试加锁 相当于刷新ttl时间
ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
return;
}
if (ttl >= 0L) {
// getLatch()方法 信号量获取一个许可 等待超时时间 这个等待会让出CPU 超时时间结束会回到while循环 再次尝试加锁
// public Semaphore getLatch() {
// return this. Latch;
// }
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
this.getEntry(threadId).getLatch().acquire();
}
}
} finally {
this.unsubscribe(future, threadId);
}
}
}
//
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// 当RFuture方法执行完成后 会执行 addListener方法
ttlRemainingFuture.addListener(new FutureListener<Long>() {
public void operationComplete(Future<Long> future) throws Exception {
if (future.isSuccess()) {
Long ttlRemaining = (Long)future.getNow();
if (ttlRemaining == null) {
· //刷新超时时间
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
return ttlRemainingFuture;
}
}
//刷新超时时间
private void scheduleExpirationRenewal(final long threadId) {
if (!expirationRenewalMap.containsKey(this.getEntryName())) {
// 延时任务 run方法会在延时后执行
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
// RFuture执行完成后执行addListener方法
RFuture<Boolean> future = RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断主线程加的锁是否还存在 (name, this.getLockName(threadId))
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
// 重新设置主线程超时时间为 this.internalLockLeaseTime
redis.call('pexpire', KEYS[1], ARGV[1]);
// 返回true
return 1;
end;
// 返回false
return 0;",
Collections.singletonList(RedissonLock.this.getName()), new Object[]{RedissonLock.this.internalLockLeaseTime, RedissonLock.this.getLockName(threadId)});
// RFuture执行完成后执行
future.addListener(new FutureListener<Boolean>() {
public void operationComplete(Future<Boolean> future) throws Exception {
RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
if (!future.isSuccess()) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
} else {
// 判断RFuture返回结果
if ((Boolean)future.getNow()) {
// 调用自己 延时执行 即锁续命
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); //延时时间 = this.internalLockLeaseTime / 3
if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
task.cancel();
}
}
}
// 尝试加锁
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,
// Lua脚本
// 判断redis中是否存在key1的值
"if (redis.call('exists', KEYS[1]) == 0) then
// 不存在的话 调用hset(key1,value2)
redis.call('hset', KEYS[1], ARGV[2], 1);
// 设置超时时间为 value1
redis.call('pexpire', KEYS[1], ARGV[1]);
// 返回 null
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 再来一个线程 返回当前线程剩余超时时间
return redis.call('pttl', KEYS[1]);",
//Key1 == this.getName() value1 == this.internalLockLeaseTime value2 == this.getLockName(threadId))
Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
// 解锁
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果当前线程还存在
"if (redis.call('exists', KEYS[1]) == 0) then
// 不存在 发布解锁消息 LockPubSub.unlockMessage
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
// 判断这把锁是不是自己加的
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
// this.getLockName(threadId)) == 1 则 counter = 1 - 1 = 0
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
// delete 并发布解锁消息
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;",
Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});
}
// LockPubSub类
public static final Long unlockMessage = 0L;
// 消息方法
protected void onMessage(RedissonLockEntry value, Long message) {
if (message.equals(unlockMessage)) {
// 如果消息时释放锁 调用getLatch() 信号量 唤醒等待的线程
value.getLatch().release();
while(true) {
Runnable runnableToExecute = null;
synchronized(value) {
Runnable runnable = (Runnable)value.getListeners().poll();
if (runnable != null) {
if (value.getLatch().tryAcquire()) {
runnableToExecute = runnable;
} else {
value.addListener(runnable);
}
}
}
if (runnableToExecute == null) {
return;
}
runnableToExecute.run();
}
}
}