日常redis用的比较频繁,但是还没对redis做一些笔记.今天想起来,正好写一下.
ps : redis的安装、基本概念、配置就不做说明.
下面进入正题.
redis为单线程,那么为什么设计单线程?有什么优点呢?
官方给的解释貌似是因为redis很快,没必要为了多线程而损失单线程的优点.当然,也可能是谬论!>_<
单进程单线程好处
1> 代码更清晰,处理逻辑更简单
2> 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
3> 不存在多进程或者多线程导致的切换而消耗CPU
单进程单线程弊端
1> 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;当然这个问题可以通过创建多个redis实例来解决.
redis默认字符串以及优点
redis默认字符串为简单动态字符串(simple dynamic string)sds
与c语言字符串优点[估计有人会说,redis怎么扯出来c语言了 >_<]
优点 :
1> 计算length:c中不记录length,要遍历数组才能得到string的长度,时间复杂度为O(n);SDS则记录了lenth,时间复杂度则为O(1)
2> 缓冲区溢出:C在字符串创建时,已经分配了指定的缓冲区内存。在修改的时候,都需要手动的判断内存大小是否允许修改后的值。如果忽视了手动判断大小,直接拼接则容易造成,缓冲区内存溢出。SDS则会在修改时,会自动判断大小,从而避免了缓冲区溢出的可能。
3> 减少了内存重新分配次数。sds加入了free,采用空间预分配和惰性空间释放来优化分配次数。不想java一样,String类型,放在了常量池中。free 用来记录修改后,未使用的大小。这就是为了方便,惰性空间释放。
4> 二进制安全的
5> 兼容部分C的函数,尾部也保留空字符
与java中String有所区别
1> java 中string是final的,java中string被存放在常量池中,不能修改,因为java内存由虚拟机动态内存管理。
redis雪崩
问题 :
缓存在同一时间内大量键过期(失效),接着来的一大波请求瞬间都落在了数据库中导致连接异常。
解决 :
1> 像解决缓存穿透一样加锁排队,实现同上;
2> 建立备份缓存,缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有读B,并且更新A缓存和B缓存;
3> 设置缓存超时时间的时候加上一个随机的时间长度,比如这个缓存key的超时时间是固定的5分钟加上随机的2分钟,这样可从一定程度上避免雪崩问题;
redis缓存穿透
问题 :
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决 :
1> 布隆过滤 : 对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
适用范围 : 可以用来实现数据字典,进行数据的判重,或者集合求交集
2> 缓存空对象. 将 null 变成一个值.
1.空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
2.缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
还有另外一种缓存穿透 :
高并发同时请求,可能都会从数据库获取,想达到第一次请求从数据库中获取,其他9999个请求从redis中获取这种效果。
解决 :
从redis获取使用双重检测锁[至于具体什么是双重检测锁,我想用单例的都知道吧,就不过多进行说明 >_<]
redis setex和set 的区别
SETEX命令 : SETEX key seconds value 将value关联到key, 并将key的生存时间设为seconds(以秒为单位)。如果key 已经存在,SETEX将重写旧值;
相当于 : SET('key', 'value'); EXPIRE('key','seconds'); # 设置生存时间
redis分布式锁机制
分布式锁机制网上有大量说明,这里我就只贴一下核心代码吧.
简单说明一下 : redis锁使用的lua脚本,set使用的setnx.实现方案看代码就行,没什么复杂的就不过多说明.
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"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]);",
springboot使用RedisTemplate整合redis锁(springboot整合redis不做说明) :
RediHelp :
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import redis.clients.jedis.Jedis;
public class RediHelp {
protected RedisTemplate<String, String> redisTemplate;
private static final Logger log = LoggerFactory.getLogger(RediHelp.class);
public RedisTemplate<String, String> getRedisTemplate() {
return redisTemplate;
}
/**
* 获取redis的分布式锁,内部实现使用了redis的setnx。如果锁定失败返回null,如果锁定成功则返回RedisLock对象,调用方需要调用RedisLock.unlock()方法来释放锁
* <span style="color:red;">此方法在获取失败时会自动重试指定的次数,由于多次等待会阻塞当前线程,请尽量避免使用此方法</span>
* @param key 要锁定的key
* @param expireSeconds key的失效时间
* @param maxRetryTimes 最大重试次数,如果获取锁失败,会自动尝试重新获取锁;
* @param retryIntervalTimeMillis 每次重试之前sleep等待的毫秒数
* @return 获得的锁对象(如果为null表示获取锁失败),后续可以调用该对象的unlock方法来释放锁.
*/
private static final String COMPARE_AND_DELETE =
"if redis.call('get',KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call('del',KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
// 锁失效时间
private static final long expireSeconds = 3;
// 重试次数
private static final int maxRetryTimes = 1;
private static final long retryIntervalTimeMillis = 1;
public RedisLock getLock(final String key){
final String value = "-1";
int maxTimes = maxRetryTimes + 1;
for(int i = 0;i < maxTimes; i++) {
String status = getRedisTemplate().execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
Jedis jedis = (Jedis) connection.getNativeConnection();
String status = jedis.set(key, value, "nx", "ex", expireSeconds);
return status;
}
});
//抢到锁
if ("OK".equals(status)) {
return new RedisLockInner(getRedisTemplate(), key, value);
}
if(retryIntervalTimeMillis > 0) {
try {
Thread.sleep(retryIntervalTimeMillis);
} catch (InterruptedException e) {
log.warn("error:{}.",e.getMessage());
Thread.currentThread().interrupt();
break;
}
}
if(Thread.currentThread().isInterrupted()){
break;
}
}
return null;
}
private class RedisLockInner implements RedisLock{
private RedisTemplate<String, String> redisTemplate;
private String key;
private String expectedValue;
protected RedisLockInner(RedisTemplate<String, String> redisTemplate, String key, String expectedValue){
this.redisTemplate = redisTemplate;
this.key = key;
this.expectedValue = expectedValue;
}
/**
* 释放redis分布式锁
*/
@Override
public void unlock(){
List<String> keys = Collections.singletonList(key);
redisTemplate.execute(new DefaultRedisScript<>(COMPARE_AND_DELETE, String.class), keys, expectedValue);
}
@Override
public void close() throws Exception {
this.unlock();
}
}
}
RedisLock :
/**
* Redis的分布式锁对象
*/
public interface RedisLock extends AutoCloseable {
/**
* 释放分布式锁
*/
void unlock();
}
RedisUtil :
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* 增加redis分布式锁机制
// 获取锁[默认锁过期时间为3秒]
RedisLock redisLock = null;
try{
redisLock = redisUtil.getLock("redis_lock");
if(redisLock!=null) {
// 获取到锁,执行业务逻辑代码
}else {
// 未获取到锁
}
}cache(Exception e){
}finally{
// 释放锁
if(redisLock!=null){
redisLock.unlock();
}
}
* @author anyunfeng
*
*/
@Component
public class RedisUtil extends RediHelp{
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public RedisTemplate<String, String> getRedisTemplate() {
return redisTemplate;
}
}
redis支持哪些类型
这个其实没什么好说的 >_<
string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
mysql与redis实现事务同步
1> 分别处理
针对某些对数据一致性要求不是特别高的情况下,可以将这些数据放入Redis,请求来了直接查询Redis,例如近期回复、历史排名这种实时性不强的业务。而针对那些强实时性的业务,例如虚拟货币、物品购买件数等等,则直接穿透Redis至MySQL上,等到MySQL上写入成功,再同步更新到Redis上去。这样既可以起到Redis的分流大量查询请求的作用,又保证了关键数据的一致性。
2> 高并发情况下
此时如果写入请求较多,则直接写入Redis中去,然后间隔一段时间,批量将所有的写入请求,刷新到MySQL中去;如果此时写入请求不多,则可以在每次写入Redis,都立刻将该命令同步至MySQL中去。这两种方法有利有弊,需要根据不同的场景来权衡
3> 基于订阅binlog的同步机制
阿里巴巴的一款开源框架canal,提供了一种发布/ 订阅模式的同步机制,通过该框架我们可以对MySQL的binlog进行订阅,这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。值得注意的是,binlog需要手动打开,并且不会记录关于MySQL查询的命令和操作。
redis的一些面试题
Redis哈希槽的概念 :
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
Redis中的管道有什么用 :
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
Redis如何做内存优化 :
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面.
Redis常见性能问题和解决方案 :
1> Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
2> 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
3> 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
4> 尽量避免在压力很大的主库上增加从库
5> 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...
Redis提供了哪几种持久化方式 :
1> RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储.
2> AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大.
3> 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
4> 你也可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
五种I/O模型介绍 :
1> 阻塞I/O模型 : 老李去火车站买票,排队三天买到一张退票。耗费:在车站吃喝拉撒睡 3天,其他事一件没干。
2> 非阻塞I/O模型 : 老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。 耗费:往返车站6次,路上6小时,其他时间做了好多事。
3> I/O复用模型 :
1> select/poll : 老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。 耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
2> epoll : 老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。 耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话
4> 信号驱动I/O模型 : 老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。 耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话
5> 异步I/O模型 : 老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。 耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话
1同2的区别是:自己轮询
2同3的区别是:委托黄牛
3同4的区别是:电话代替黄牛
4同5的区别是:电话通知是自取还是送票上门
多路复用API,多路复用最常用的就是select和epoll,多路复用的最大的特点就是多路,一个select可以同时处理多个套接字的读写请求,这时候IO的阻塞点往往是多路复用的API,而不是IO操作本身
在redis中的IO模型就是使用多路复用API进程客户端的检测,多路复用API可以同时监听多个客户端的读写操作,多路复用API会检测客户端的请求是读操作还是写操作,往往API会有一个timeout,在这个时间内redis线程会阻塞,进行套接字的监听,redis会给每个客户端套接字匹配一个指令队列,按照队列进行处理,同时也会将操作结果放到输出队列
正是通过这种多路复用的思想进行非阻塞的IO,这样才保证了redis的高效