目录
Redis
Redis是一个功能十分强大的“缓存”,他会运行在内存中,并且把数据存储在硬盘上,在程序正常执行时在内存操作,意外关闭之后重启可以通过硬盘上的存储内容重启。
Redis配置文件
对于企业中经常使用Redis,配置文件的修改是必须的,以下列出几个常用配置参数,可在redis安装目录下找到redis.conf进行修改。
参数说明
redis.conf 配置项说明如下:
Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no
当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定 pidfile /var/run/redis.pid
指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字 port 6379
绑定的主机地址 bind 127.0.0.1 5.当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能 timeout 300
指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose loglevel verbose
日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null logfile stdout
设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id databases 16
指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 save <seconds> <changes> Redis默认配置文件中提供了三个条件: save 900 1 save 300 10 save 60 10000 分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大 rdbcompression yes
指定本地数据库文件名,默认值为dump.rdb dbfilename dump.rdb
指定本地数据库存放目录 dir ./
设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步 slaveof <masterip> <masterport>
当master服务设置了密码保护时,slav服务连接master的密码 masterauth <master-password>
设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH <password>命令提供密码,默认关闭 requirepass foobared
设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息 maxclients 128
指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区 maxmemory <bytes>
指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no appendonly no
指定更新日志文件名,默认为appendonly.aof appendfilename appendonly.aof
指定更新日志条件,共有3个可选值: no:表示等操作系统进行数据缓存同步到磁盘(快) always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全) everysec:表示每秒同步一次(折衷,默认值) appendfsync everysec
指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制) vm-enabled no
虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享 vm-swap-file /tmp/redis.swap
将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0 vm-max-memory 0
Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值 vm-page-size 32
设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。 vm-pages 134217728
设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4 vm-max-threads 4
设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启 glueoutputbuf yes
指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法 hash-max-zipmap-entries 64 hash-max-zipmap-value 512
指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍) activerehashing yes
指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件 include /path/to/local.conf
Redis启动
cd /usr/local/bin
ps -ef | grep redis //查看redis是否已经启动
redis-server ../../myredis/redis.conf //使用特定文件夹下的redis.conf启动redis
redis-cli -p 6379 //进入redis客户端
ping //检测连接是否成功
redis-server
Redis的持久化
RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里,保存为一个dump.rdb文件
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方 式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
配置位置
指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 save <seconds> <changes> Redis默认配置文件中提供了三个条件: save 900 1(一般只保留这个) save 300 10 save 60 10000 分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
一般情况下,RDB并不提供过于细粒度的存储,所以只是存个大概,一般情况下只保留第一行也就是:save 900 1做为配置,也就是15分钟存储一次。
触发方式
通过配置的情况进行触发,或者手动输入 save(阻塞进行保存)或者bgsave(异步保存不阻塞)。或者输入了flushall命令或者shutdown也会触发RDB的存储。但是注意,flushall会导致存储一个空的dump.rdb文件。
恢复方式
将dump.rdb文件拷贝到redis安装目录下即可。可以在redis中使用config get dir获取安装目录。
停止方式
动态停止所有RDB保存规则的方式:redis-cli config set save ""
也就是主动修改配置文件
AOF
AOF和RDB相辅相成,为了提供更细粒度的硬盘存储而生。二者可以同时启动,并且恢复数据优先使用AOF的,因为它的数据存储丢失情况更少
AOF以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。生成的是appendonly.aof文件。简而言之,每隔一秒记录你的所有读操作,在重启之后将这些操作全部从头到尾再执行一遍
配置方式
指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no。修改为true即可开启AOP appendonly no
指定更新日志文件名,默认为appendonly.aof,一般不修改 appendfilename appendonly.aof
指定更新日志条件,共有3个可选值: no:表示等操作系统进行数据缓存同步到磁盘(快) always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全) everysec:表示每秒同步一次(折衷,默认值)一般不修改,都是用everysec appendfsync everysec
AOF恢复方式
将aof保存的appendonly.aof拷贝到安装目录重启即可。
AOF异常修复
因为存在系统异常宕机导致的部分数据写入不完整导致的语法错误,那么这样的AOF文件如果存在会因为执行失败导致redis无法启动。对此,Redis提供了自修复命令:Redis-check-aof --fix。修复之后就可以正常启动了。RDB也拥有相同的功能。
AOF的rewrite
AOF由于是记录所有的写操作,那么自然不可避免生成的aof文件十分庞大,那么就需要对这个文件进行合理的压缩。当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof
rewrite原理
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
触发机制
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。该阈值的大小可以在配置文件中修改,一般使用3G起,64MB能装下个啥呀。
总结
| RDB | AOF | |
|---|---|---|
| 数据一致性 | 会丢失最后一次备份的修改 | 会丢失最后1s的修改 |
| 运行效率 | 每15分钟备份1次,快 | 每1s备份一次,慢 |
| 恢复效率 | 文件较小,快 | 文件较大,慢 |
建议同时开启,并且使用主从复制策略
Redis的事务
事务就是一系列操作的批处理,将它们串行执行
使用方式
multi // 开启本次事务
set id 12 // 正常的操作
get id
incr t1
incr t1
get t1
exec // 提交事务
multi命令开启并且在最后exec提交
事务的本质
Redis的事务在注册后并不会真正意义上的执行,也就不存在回滚的说法。只有在提交之后,才串行的执行队列中所有的命令。
四种执行情况
-
正常执行
-
放弃事务
也就是在最后不执行exec命令,而执行discard命令,即放弃所有事务的执行
-
集体连坐
如果在事务中出现了错误的指令导致直接报错那么整个事务不管之后之前的命令究竟是什么,提交exec之后都不会有任何效果
-
冤有头债有主
与集体连坐不同的地方在于,如果命令合法,但是执行时报错,那么其他命令都可以正常进行,只有执行报错的命令执行失败
监控(锁)
使用watch命令可以实现乐观锁,也就是类似CAS操作。
watch money //监控money的值,也就是缓存一下
multi //开启事务
/ 其他业务操作 /
exec //提交 如果此时money的实际值和缓存时不同,那么就直接什么都不做,否则才提交
主从复制
对于redis多端运行时,一台主机作为Master,两台主机作为slave。Master的数据会同步复制到slave,并且只有Mater能set,slave并不能执行写操作,只可以执行读操作。实现读写分离以及多数据备份容灾控制
使用方式
-
配从不配主
-
从库执行:slaveof 主库ip地址 主库端口 来绑定主库
-
修改配置文件细节操作
(1) 打开从库的daemonize yes
(2) 修改pid配置(不同服务器交互可以不改)
(3) 指定端口(不同服务器交互可以不改)
(4) 指定log文件名字 logfile "mylogProt.log"
(5) 指定dump.rdb的名字(可选)
-
常用方式
(1) 一仆二主:一台Master用于写两台Slave用于读
(2) 薪火相传:为了降低Master对于多台Slave执行数据同步的写压力,让一台Master连接一个Slave,再由连接Master的Slave连接另一台Slave。让Master的写压力降低一半
(3)反客为主:当Master宕机之后,剩余的Slave默认原地待命等待Master复活,仍可以进行读取,但是本质并不是Master所以还是不能写。只能够手动执行slaveof no one修改Slave的模式让其中一个Slave接替Master的位置。
复制原理
-
Slave启动成功连接到master后会发送一个sync命令
-
Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
-
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
-
增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
-
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
哨兵模式
反客为主的自动版本,Master宕机之后,所有Slave会自动投票选出一台主机作为新的Master,并且旧的Master恢复后自动变为Slave跟随新的Master工作。
设置
sentinel monitor 被监控数据库名字(自己起名字) Master的IP Master的端口号 胜选票数
eg:sentinel monitor myredis001 127.0.0.1 6379 1
设置本机6379端口为Master。上面最后一个数字1,表示主机挂掉后salve投票看让谁接替成为主机,得票数多少后成为主机
启动哨兵:Redis-sentinel /myredis/sentinel.conf
查看当前主机状态:info replication
复制的缺陷
复制由于需要跨设备,所以存在复制时延。
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
Java中使用Jedis操作Redis
常用操作
-
测试连通性
public class Demo01 { public static void main(String[] args) { //连接本地的 Redis 服务 Jedis jedis = new Jedis("127.0.0.1",6379); //查看服务是否运行,打出pong表示OK System.out.println("connection is OK==========>: "+jedis.ping()); } } -
5+1操作
import java.util.*; import redis.clients.jedis.Jedis; public class Test02 { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1",6379); //key Set<String> keys = jedis.keys("*"); for (Iterator iterator = keys.iterator(); iterator.hasNext();) { String key = (String) iterator.next(); System.out.println(key); } System.out.println("jedis.exists====>"+jedis.exists("k2")); System.out.println(jedis.ttl("k1")); //String //jedis.append("k1","myreids"); System.out.println(jedis.get("k1")); jedis.set("k4","k4_redis"); System.out.println("----------------------------------------"); jedis.mset("str1","v1","str2","v2","str3","v3"); System.out.println(jedis.mget("str1","str2","str3")); //list System.out.println("----------------------------------------"); //jedis.lpush("mylist","v1","v2","v3","v4","v5"); List<String> list = jedis.lrange("mylist",0,-1); for (String element : list) { System.out.println(element); } //set jedis.sadd("orders","jd001"); jedis.sadd("orders","jd002"); jedis.sadd("orders","jd003"); Set<String> set1 = jedis.smembers("orders"); for (Iterator iterator = set1.iterator(); iterator.hasNext();) { String string = (String) iterator.next(); System.out.println(string); } jedis.srem("orders","jd002"); System.out.println(jedis.smembers("orders").size()); //hash jedis.hset("hash1","userName","lisi"); System.out.println(jedis.hget("hash1","userName")); Map<String,String> map = new HashMap<String,String>(); map.put("telphone","13811814763"); map.put("address","atguigu"); map.put("email","abc@163.com"); jedis.hmset("hash2",map); List<String> result = jedis.hmget("hash2", "telphone","email"); for (String element : result) { System.out.println(element); } //zset jedis.zadd("zset01",60d,"v1"); jedis.zadd("zset01",70d,"v2"); jedis.zadd("zset01",80d,"v3"); jedis.zadd("zset01",90d,"v4"); Set<String> s1 = jedis.zrange("zset01",0,-1); for (Iterator iterator = s1.iterator(); iterator.hasNext();) { String string = (String) iterator.next(); System.out.println(string); } } } -
事务
//不加锁 import redis.clients.jedis.Jedis; import redis.clients.jedis.Response; import redis.clients.jedis.Transaction; public class Test03 { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1",6379); //监控key,如果该动了事务就被放弃 /*3 jedis.watch("serialNum"); jedis.set("serialNum","s#####################"); jedis.unwatch();*/ Transaction transaction = jedis.multi();//被当作一个命令进行执行 Response<String> response = transaction.get("serialNum"); transaction.set("serialNum","s002"); response = transaction.get("serialNum"); transaction.lpush("list3","a"); transaction.lpush("list3","b"); transaction.lpush("list3","c"); transaction.exec(); //2 transaction.discard(); System.out.println("serialNum***********"+response.get()); } } //加锁 public class TestTransaction { public boolean transMethod() { Jedis jedis = new Jedis("127.0.0.1", 6379); int balance;// 可用余额 int debt;// 欠额 int amtToSubtract = 10;// 实刷额度 jedis.watch("balance"); //jedis.set("balance","5");//此句不该出现,讲课方便。模拟其他程序已经修改了该条目 balance = Integer.parseInt(jedis.get("balance")); if (balance < amtToSubtract) { jedis.unwatch(); System.out.println("modify"); return false; } else { System.out.println("***********transaction"); Transaction transaction = jedis.multi(); transaction.decrBy("balance", amtToSubtract); transaction.incrBy("debt", amtToSubtract); transaction.exec(); balance = Integer.parseInt(jedis.get("balance")); debt = Integer.parseInt(jedis.get("debt")); System.out.println("*******" + balance); System.out.println("*******" + debt); return true; } } /** * 通俗点讲,watch命令就是标记一个键,如果标记了一个键, 在提交事务前如果该键被别人修改过,那事务就会失败,这种情况通常可以在程序中 * 重新再尝试一次。 * 首先标记了键balance,然后检查余额是否足够,不足就取消标记,并不做扣减; 足够的话,就启动事务进行更新操作, * 如果在此期间键balance被其它人修改, 那在提交事务(执行exec)时就会报错, 程序中通常可以捕获这类错误再重新执行一次,直到成功。 */ public static void main(String[] args) { TestTransaction test = new TestTransaction(); boolean retValue = test.transMethod(); System.out.println("main retValue-------: " + retValue); } } -
主从复制
public class Main{ public static void main(String[] args){ Jedis jedis_M = new Jedis( "127.0.0.1" , 6379 ); Jedis jedis_S = new Jedis( "127.0.0.1" , 6380 ); jedis_S.slaveof( "127.0.0.1",6379 );//从绑定主 jedis_M.set( "class" , "1122" );//主添加键class,值为1122 String result = jedis_S.get("class");//从读键class的值,可能会因为复制时延导致拿到为null System.out.println( result ); } }
JedisPool
Jedis池,连接池的玩法,连接Jedis不需要New对象消耗系统资源,用完再放回去就好了。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;//被volatile修饰的变量不会被本地线程缓存,对该变量的读写都是直接操作共享内存。
private JedisPoolUtil() {}
public static JedisPool getJedisPoolInstance()
{
if(null == jedisPool)
{
synchronized (JedisPoolUtil.class)
{
if(null == jedisPool)
{
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxActive(1000);//设置最大请求数量
poolConfig.setMaxIdle(32);//设置剩余多少请求时警报
poolConfig.setMaxWait(100*1000);//设置最大等待时长
poolConfig.setTestOnBorrow(true);//拿到Jedis后是否测试连通性
jedisPool = new JedisPool(poolConfig,"127.0.0.1");
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool,Jedis jedis)
{
if(null != jedis)
{
jedisPool.returnResourceObject(jedis);
}
}
public static void main(String[] args) {
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = null;
try
{
jedis = jedisPool.getResource();//拿到连接对象Jedis
jedis.set("k18","v183");
} catch (Exception e) {
e.printStackTrace();
}finally{
JedisPoolUtil.release(jedisPool, jedis);//在finally中归还Jedis
}
}
}
配置总结
JedisPool的配置参数大部分是由JedisPoolConfig的对应项来赋值的。
maxActive:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted。
maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
whenExhaustedAction:表示当pool中的jedis实例都被allocated完时,pool要采取的操作;默认有三种。 WHEN_EXHAUSTED_FAIL --> 表示无jedis实例时,直接抛出NoSuchElementException; WHEN_EXHAUSTED_BLOCK --> 则表示阻塞住,或者达到maxWait时抛出JedisConnectionException; WHEN_EXHAUSTED_GROW --> 则表示新建一个jedis实例,也就说设置的maxActive无用;
maxWait:表示当borrow一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛JedisConnectionException;
testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
testOnReturn:return 一个jedis实例给pool时,是否检查连接可用性(ping());
testWhileIdle:如果为true,表示有一个idle object evitor线程对idle object进行扫描,如果validate失败,此object会被从pool中drop掉;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;
timeBetweenEvictionRunsMillis:表示idle object evitor两次扫描之间要sleep的毫秒数;
numTestsPerEvictionRun:表示idle object evitor每次扫描的最多的对象数;
minEvictableIdleTimeMillis:表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;
softMinEvictableIdleTimeMillis:在minEvictableIdleTimeMillis基础上,加入了至少minIdle个对象已经在pool里面了。如果为-1,evicted不会根据idle time驱逐任何对象。如果minEvictableIdleTimeMillis>0,则此项设置无意义,且只有在timeBetweenEvictionRunsMillis大于0时才有意义;
lifo:borrowObject返回对象时,是采用DEFAULT_LIFO(last in first out,即类似cache的最频繁使用队列),如果为False,则表示FIFO队列;
================================================================================================================== 其中JedisPoolConfig对一些参数的默认设置如下: testWhileIdle=true minEvictableIdleTimeMills=60000 timeBetweenEvictionRunsMillis=30000 numTestsPerEvictionRun=-1
跳表
跳表是有序集合在集合内元素超过一定数量之后(可在配置文件设置),使用的特殊数据结构。跳表和默认情况下是使用的ziplist也就是压缩集合,这是一个无序集合,在数量较大时搜索速率较低。跳表是一个较为特殊的数据结构,结构如下:

对于传统的list施加了n层索引,从一层索引开始遍历,假设此时我们想要找到第14号数据,流程如下:
-
在一层索引找到15,发现目标值比它小,进入二层索引
-
二层索引从15开始找到10,发现它比目标值小,则确定了目标值就在两索引之间
-
从10位置进入数据层,开始向前搜寻,直至搜寻到目标值或第一个比它大的数字为止。
通过多级索引缩小搜寻范围,多使用最多一倍的空间,提高了接近一倍的搜寻效率
缓存穿透
在正常的业务流程中,会出现以下情况:查询的数据在数据库中就不存在
那么此时会出现一个问题,数据库中都没有的数据,对于缓存来说更是不可能有的,那么,此时如果有不法分子使用Jmater进行了1000w次(超高并发)的访问,那么实际访问情况就如下图所示:

MySQL作为效率瓶颈,并且在分布式情况下,它作为最核心的部分,绝对要保护MySQL的安全,宁愿牺牲效率
对于缓存穿透,我们拥有两个解决方案:
-
设置key null,也就是不存在的数据使用null来放入redis,这个方案会十分浪费内存空间,在严重的时候会挤掉热点数据,所以能不用就不用。
-
布隆过滤器。布隆过滤器会帮助我们处理缓存穿透问题,也就是将请求过滤,假设1000w条访问不存在数据的请求就会被布隆过滤器直接挡住,不对我们的MySQL进行施压。
缓存击穿
缓存击穿出的情况是:高并发查询了一条redis没有缓存的key

这个问题可以通过加锁解决,也就是类似单例模式的Double Check模式。缓存中没拿到就去竞争一把锁,拿到锁的才能去MySQL查,查完之后加入redis释放掉锁,剩余竞争锁的线程在查询数据库之前需要第二次确认缓存中是否真的不存在这个Key。这样就有效做到了将1000w跳请求转化为1条对MySQL施压的请求。
缓存雪崩
缓存雪崩是最麻烦的一个问题,它的情况是:高并发访问了N条redis中不存在的数据Key,或者是redis突然宕机或大量key失效导致高并发请求全部冲到了MySQL中

对于这个问题,没有太多的解决方案,因为在业务场景中,不同的key往往不受控于同一把锁(效率为上)。那么假设此时有10w个请求请求了10w个不同的key或者Redis突然自己宕机了,那么这10w个key在Redis中都没有的话,也就意味着同时有10w个请求会一起打在MySQL上,MySQL就完蛋了。
对于这个问题我们能做到的只能是:数据预热
在双11当天,最容易出现的就是缓存雪崩问题,当然阿里巴巴对于它的处理也采用了数据预热,通过关闭部分功能,将redis全部用于接下来的部分功能。然后将平时收集到容易被访问的数据预先保存到redis中。比如10w条不同的访问,我们预先存放了7w条可能会用到的key,假设命中率为50%,我们也同时化解了3.5w条请求。
Redis是一个内存数据库,提供多种持久化方式,包括RDB快照和AOF日志。RDB在指定时间间隔保存数据集,适合全量恢复;AOF记录所有写操作,保证数据不丢失。Redis支持事务,通过multi-exec批量执行命令。主从复制用于数据备份和读写分离,哨兵模式实现自动故障转移。Java中使用Jedis连接Redis,通过JedisPool管理连接池。
392

被折叠的 条评论
为什么被折叠?



