是什么?
开源的,C语言写的,单线程的,高性能的,key-value的内存数据库,基于内存运行并支持持久化的nosql数据库
安装
下载:官网/github/yml
linux单机版安装步骤:
1、下载tar.gz (https://github.com/antirez/redis/releases)
2、tar -zxvf 解压
3、yum install gcc 安装gcc环境,因为redis是c语言写的,所以需要环境
4、先 cd redis 再 make
5、先cd src 再 make install
6、修改redis.conf (bind、保护模式、dir设置持久化保存的目录、守护线程、aof配置、rdb配置,密码)
7、服务端:redis-server redis.conf
客户端: 可视化或者命令行: redis-cli -h ip -p 6379
集群搭建:
1、配置三主三从的redis节点 建立6个文件夹,分别放置不同的redis.conf
2、修改下列配置:注意port dir 等基础配置,不要有文件冲突
port 7000 daemonize yes pidfile /var/run/redis_7000.pid cluster-enabled yes cluster-node-timeout 15000 cluster-config-file nodes-7000.conf
3、第一个修改好了,将7000的conf复制给其他节点
sed 's/7000/7001/g' redis7000/redis.conf > redis7001/redis.conf
4、分别启动所有的节点 redis-server redis-cluster/redis7005/redis.conf
5、--cluster help查看帮助 redis-cli --cluster help
6、create创建集群,自动分片
redis-cli create ip:port ip2:port2 ... ipN:portN --cluster-replicas 1 -a 123456 1代表主从比例1:1设置
持久化理解:
持久化的本质就是将内存中数据,写到磁盘中进行保存,以提高数据的安全性,尽量减少因宕机等造成数据丢失的风险
redis的持久化分为rdb和aof
rdb:
原理: redis会单独fork一个子进程,来进行数据写入磁盘的io操作。这个子进程的所有数据和主进程保持一致。子进程会创建一个临时文件,将数据写入,等数据写入完成后,将上一次备份的rbd文件替换。rdb操作时,会阻塞主进程的io操作
rdb位置:redis.conf 中的 dir配置,默认 ./
什么时候创建子进程,进行rbd操作:
手动触发:客户端调用save(同步) bgsave(异步) shoutdown(没有开启aof操作时) flushall
自动触发:redis.conf 配置的save达到触发条件时 默认下列配置,生产环境不需要这么启动的这么频繁
save 900 1 save 300 10 save 60 10000 save 时间(秒) 修改的数据量
aof:
原理:将redis的操作日志(事物处理),以追加的方式写入文件,读操作不记录。
触发机制:redis.conf 中设置 appendonly yes 开启aof (apend only file)
规则:# appendfsync no(操作系统控制,快,不可控)/ always(时时,一条写操作追加一次 慢 安全)/ everysec (sec:秒 每秒执行一次 均衡)
重写规则(瘦身压缩,将规则数据转换为二进制文件): auto-aof-rewrite-percentage 100 (重写比例)
auto-aof-rewrite-min-size 64mb (重写最小文件大小)思考: 以前系统开了rdb,保存了部分数据,后面开启了aof,数据是否可以加载进来?
缓存起到了什么作用:
1、缓存查询比较快,提高接口响应速度
2、减少了查询数据库,调用接口的次数,对服务进行降压
缓存的问题:
1、缓存粒度问题
通俗的说,缓存粒度问题,就是在缓存数据时,是缓存全部数据,还是缓存部分数据。
缓存全部数据,粒度粗,通用性好,可用性强,代码相对简单,但是会有大量的无用数据,造成空间浪费,网络带宽浪费。需要综合考虑,设置缓存的粒度
2、缓存穿透问题
个人理解就是,访问一个根本不存在的数据,请求穿过缓存,还是直接访问到了数据库
当恶意攻击时,请求没办法在缓存中找到对应数据,并且调到了数据库或者接口请求中,缓存完全没有达到设计之初希望的作用,所以需要处理
解决方案:
一、缓存空对象,请求缓存查不到,请求数据库,请求数据库查不到的时候,设置空对象进缓存,下次访问可以拦截到该请求,避免重复调用
存在的问题: 1、缓存了不存在的数据,空间浪费,可以设置失效时间,但是失效时间过期后,还是存在穿透问题
2、一直切换不一样的,随机的不存在的数据,还是会重复调用到底层内部,并且造成空间的浪费,只能 解决部分问题,穿透问题没有完全解决
二、goolgle的guava包下的BloomFilter
解决的问题: 先往bloomFilter内部插入所有正确的值,查询缓存前先判断是否在bloomFilter内部,存在的话,就放行查询底层,否则可以直接返回
存在的问题: 内存中设置数据,处理数据,空间利用大,不支持大数据量处理
在分布式环境中,都在本地内存中,在集群环境不好维护处理
重启就失效了,利用率低
设置缓存的代码如下:
/**
* funnel:存储数据类型 expectedInsertions:数据存储容量 fpp:误判率(误判率影响整体性能
*/
BloomFilter bf =BloomFilter.create(Funnels.integerFunnel(),10000,0.01);
// 初始化1000000条数据到过滤器中
for (int i = 0; i < 10000; i++) {
bf.put(i);
}
//是否有匹配不上的
for (int i = 0; i < 10000; i++) {
if (!bf.mightContain(i)) {
System.out.println("有坏人逃脱了~~~");
}
}
// 匹配不在过滤器中的10000个值,有多少匹配出来
int count = 0;
for (int i = 10000; i < 10000 + 10000; i++) {
if (bf.mightContain(i)) {
count++;
}
}
结果: 没有存在匹配不上的,0.01个误判的,在缓存穿透问题上,误判仅是指能够查询到数据库中,所以不影响功能,业务上可以接受
实现原理:
根据误判率,通过内部算法,设置一个长度大小合适的数组,hash函数合适的个数,来实现针对某一个key,利用多个hash函数散列,同时设置值,当判断是否存在布隆过滤器判断时,根据这个key,同时判断所有的hash函数位置上均有值,就算命中。
源码如下,虽然我也没看懂,但是可以拷贝借鉴:
long numBits = optimalNumOfBits(expectedInsertions, fpp);
int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
误判率的解释:
bloomFilter的误判率:bloomFilter本质是通过设置的误判率来设置对应的数组容量大小和hash函数个数,实现低空间,高利用的目的,但是会出现,判断数组中的数据全部被其他key值设置为1,恰巧不存在的key,所hash的所有的数组位置均为1,产生误判情况
三、利用redis来实现布隆过滤器
1、利用redis的bit来实现一个布隆过滤器,api: setbit key index value getbit key index bitcount key
2、代码涉及逻辑: 借鉴google的计算bit数组大小,hash函数个数的算法,来控制redis的使用空间大小和错误率
init初始化,获取所有的正确数据,利用setbit,根据key的hash值,取余,得到多个index,往指定 的key设置数据为1,设置到redis中
查询前先利用getbit 查询一把redis
解决问题: 1、将本地内存的大小,迁移出了业务模块,放在中间件处理,解耦,可扩展
2、 分布式环境可以使用
3、重启数据不会丢失
存在的问题: 原来都是在内存处理,现在需要额外的网络io, 性能要低于google的
redis分布式锁:
大概看了下redisson实现redis lock的代码
RLock lock = redisson.getLock();
lock.lock();
lock.unlock();
public void lock() {
try {
lockInterruptibly();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// try {
// return tryLockAsync(waitTime, leaseTime, unit).get();
// } catch (ExecutionException e) {
// throw new IllegalStateException(e);
// }
long newLeaseTime = -1;
if (leaseTime != -1) {
newLeaseTime = unit.toMillis(waitTime)*2;
}
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime != -1) {
remainTime = unit.toMillis(waitTime);
}
long lockWaitTime = calcLockWaitTime(remainTime);
int failedLocksLimit = failedLocksLimit();
List<RLock> acquiredLocks = new ArrayList<>(locks.size());
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
if (waitTime == -1 && leaseTime == -1) {
lockAcquired = lock.tryLock();
} else {
long awaitTime = Math.min(lockWaitTime, remainTime);
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (RedisResponseTimeoutException e) {
unlockInner(Arrays.asList(lock));
lockAcquired = false;
} catch (Exception e) {
lockAcquired = false;
}
if (lockAcquired) {
acquiredLocks.add(lock);
} else {
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
if (failedLocksLimit == 0) {
unlockInner(acquiredLocks);
if (waitTime == -1 && leaseTime == -1) {
return false;
}
failedLocksLimit = failedLocksLimit();
acquiredLocks.clear();
// reset iterator
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
failedLocksLimit--;
}
}
if (remainTime != -1) {
remainTime -= System.currentTimeMillis() - time;
time = System.currentTimeMillis();
if (remainTime <= 0) {
unlockInner(acquiredLocks);
return false;
}
}
}
if (leaseTime != -1) {
List<RFuture<Boolean>> futures = new ArrayList<>(acquiredLocks.size());
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = ((RedissonLock) rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture : futures) {
rFuture.syncUninterruptibly();
}
}
return true;
}
...........中间获取方法省略,看关键方法
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
// 直接使用LUA脚本,获取数据
// KEYS[1] 传递过来创建锁的key
// ARGV[1] 过期时间
// ARGV[2] 客户端id
/**
* 第一个if,判断key1 是不是exist =0 不存在,直接hset 并设置expire时间
* 第二个if,判断key1 和客户端id是不是都匹配上,匹配上加1 并且设置失效时间
* 否则 直接返回剩余的过期时间
*/
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"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]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
// 未获取到锁,会自旋等待拿锁,一直重试
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
}
// unlock的lua脚本
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
redis的事务:
使用事务的方式: 开启事务:MULTI 提交事务:EXEC
事务执行内部,如果语法不对,直接报错,那么exec提交时,会全部回滚
其中一个如果语法编译通过,执行报错。exec提交时,会回滚有问题的那个,其他正常提交