1 redis缓存常见问题
相关问题是:缓存击穿、缓存穿透和缓存雪崩。简易程度是从后向前,因此我们先讲缓存雪崩,再讲缓存穿透,再讲缓存击穿。
1.1 缓存雪崩
是指在同一时刻,缓存中的所有key全部失效!如果出现高并发,则直接查询数据库,会导致系统崩溃!
解决方案:
缓存的key,不要设置成同一个过期时间!
1.2 缓存穿透
是指用户查询一个数据库中根本不存在的数据(数据库中没有该记录),那么我们做缓存的时候,不向缓存中放入数据的话!会导致缓存穿透!
解决方案:
将null放入缓存,同时这个key的过期时间不能永久!设置一个相对较短时间!(其实以十分钟为单位就差不多了)
1.3 缓存击穿
是指缓存中的某一个key失效,如果出现高并发,则直接查询数据库,会导致系统崩溃!
解决方案:
加锁:分布式锁!下面讲的是基于redis实现分布式锁!
2 分布式锁
我们要知道实现分布式锁的目的:防止缓存击穿(分布式情况下,单击的话本地锁就可以了)
2.1 本地锁的局限性
之前,我们学习过synchronized及lock锁,这些锁都是本地锁。接下来写一个案例,演示本地锁的问题
2.1.1 编写测试代码
写一个测试控制器:在service-product中的TestController中添加测试方法
/*** @Description TODO*/@Api(tags = "测试接口")@RestController@RequestMapping("admin/product/test")public class TestController {@Autowiredprivate TestService testService;@GetMapping("testLock")public Result testLock() {testService.testLock();return Result.ok();}}
接口及实现类
/
1. 在缓存中存储一个num 初始值为 0
2. 利用缓存的StringRedisTemplate 获取到的当前的num 值
3. 如果num 不为空,则需要对当前值 进行 + 1 操作
4. 如果num 为空,则返回即可!
*/ 这个是实现类中需要做的事情,其中第一步:是在redis中设置num = 0,即set num = 0;
public interface TestService {void testLock();}@Servicepublic class TestServiceImpl implements TestService {@Autowiredprivate StringRedisTemplate redisTemplate;/** 1. 在缓存中存储一个num 初始值为 0* 2. 利用缓存的StringRedisTemplate 获取到的当前的num 值* 3. 如果num 不为空,则需要对当前值 进行 + 1 操作* 4. 如果num 为空,则返回即可!* */@Overridepublic void testLock() {// 查询redis中的num值String value = (String)this.redisTemplate.opsForValue().get("num");// 没有该值returnif (StringUtils.isBlank(value)){return ;}// 有值就转成成intint num = Integer.parseInt(value);// 把redis中的num值+1this.redisTemplate.opsForValue().set("num", String.valueOf(++num));}}}
2.1.2 使用ab工具测试
Linux安装ab测试工具:yum install -y httpd-tools
ab工具测试语法:ab -n(一次发送的请求数) -c(请求的并发数) 访问路径
ab工具实战(测试如下:5000请求,100并发):ab -n 5000 -c 100 http://192.168.200.1:8206/admin/product/test/testLock
启动service-product模块,然后用工具连接Linux执行上述命令测试。
测试结果:
[root@linux01 ~]# ab -n 5000 -c 100 http://192.168.200.1:8206/admin/product/test/testLockThis is ApacheBench, Version 2.3 <$Revision: 1430300 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking 192.168.200.1 (be patient)Completed 500 requestsCompleted 1000 requestsCompleted 1500 requestsCompleted 2000 requestsCompleted 2500 requestsCompleted 3000 requestsCompleted 3500 requestsCompleted 4000 requestsCompleted 4500 requestsCompleted 5000 requestsFinished 5000 requestsServer Software:Server Hostname: 192.168.200.1Server Port: 8206Document Path: /admin/product/test/testLockDocument Length: 53 bytesConcurrency Level: 100Time taken for tests: 16.606 secondsComplete requests: 5000Failed requests: 0Write errors: 0Total transferred: 790000 bytesHTML transferred: 265000 bytesRequests per second: 301.09 [#/sec] (mean)Time per request: 332.121 [ms] (mean)Time per request: 3.321 [ms] (mean, across all concurrent requests)Transfer rate: 46.46 [Kbytes/sec] receivedConnection Times (ms)min mean[+/-sd] median maxConnect: 1 79 67.0 61 407Processing: 17 179 128.3 143 3757Waiting: 11 131 106.8 95 3566Total: 59 258 151.3 223 3759Percentage of the requests served within a certain time (ms)50% 22366% 29875% 33780% 36290% 45195% 56598% 64499% 698100% 3759 (longest request)[root@linux01 ~]#
查看redis值:显然是由并发问题的!
List of unsupported commands: DUMP, RESTORE, AUTHConnecting ...Connected.最新商城项目:0>set num 0"OK"最新商城项目:0>get num"326"最新商城项目:0>
2.1.3 使用本地锁
很简单,在方法上加锁即可!
@Servicepublic class TestServiceImpl implements TestService {@Autowiredprivate StringRedisTemplate redisTemplate;/** 1. 在缓存中存储一个num 初始值为 0* 2. 利用缓存的StringRedisTemplate 获取到的当前的num 值* 3. 如果num 不为空,则需要对当前值 进行 + 1 操作* 4. 如果num 为空,则返回即可!* */@Overridepublic synchronized void testLock() {// 查询redis中的num值String value = (String)this.redisTemplate.opsForValue().get("num");// 没有该值returnif (StringUtils.isBlank(value)){return ;}// 有值就转成成intint num = Integer.parseInt(value);// 把redis中的num值+1this.redisTemplate.opsForValue().set("num", String.valueOf(++num));}}
依然将redis中的num设置为0.
再次压测:测试就是正确的!
最新商城项目:0>set num 0"OK"最新商城项目:0>get num"5000"最新商城项目:0>
完美!与预期一致,是否真的完美?
接下来再看集群情况下,会怎样?
2.1.4 本地锁问题演示锁
接下来启动8206 8216 8226 三个运行实例。
运行多个service-product实例:
server.port=8216
server.port=8226
运行方法演示一个:

注意:同时还要启动网关,这样才能访问这个集群!同时num设置为0!
通过网关压力测试:
启动网关:
ab -n 5000 -c 100 http://192.168.200.1/admin/product/test/testLock
[root@linux01 ~]# ab -n 5000 -c 100 http://192.168.200.1/admin/product/test/testLockThis is ApacheBench, Version 2.3 <$Revision: 1430300 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking 192.168.200.1 (be patient)Completed 500 requestsCompleted 1000 requestsCompleted 1500 requestsCompleted 2000 requestsCompleted 2500 requestsCompleted 3000 requestsCompleted 3500 requestsCompleted 4000 requestsCompleted 4500 requestsCompleted 5000 requestsFinished 5000 requestsServer Software:Server Hostname: 192.168.200.1Server Port: 80Document Path: /admin/product/test/testLockDocument Length: 64 bytesConcurrency Level: 100Time taken for tests: 52.790 secondsComplete requests: 5000Failed requests: 168(Connect: 0, Receive: 0, Length: 168, Exceptions: 0)Write errors: 0Total transferred: 1440840 bytesHTML transferred: 320840 bytesRequests per second: 94.71 [#/sec] (mean)Time per request: 1055.804 [ms] (mean)Time per request: 10.558 [ms] (mean, across all concurrent requests)Transfer rate: 26.65 [Kbytes/sec] receivedConnection Times (ms)min mean[+/-sd] median maxConnect: 0 10 42.1 1 413Processing: 7 749 1140.0 390 15340Waiting: 0 746 1136.7 387 14720Total: 7 759 1161.3 403 15342Percentage of the requests served within a certain time (ms)50% 40366% 72575% 94780% 111390% 162995% 242598% 425799% 7047100% 15342 (longest request)[root@linux01 ~]#
查看redis:
Connected.最新商城项目:0>get num"2370"最新商城项目:0>
集群情况下又出问题了!!!
以上测试,可以发现:
本地锁只能锁住同一工程内的资源,在分布式系统里面都存在局限性。
此时需要分布式锁。。
2.2 分布式锁实现的解决方案
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等)
- 基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
1. 性能:redis最高
2. 可靠性:zookeeper最高
这里,我们就基于redis实现分布式锁。如果是银行业务,可能需要用zookeeper,安全性更高,但是性能略差!
2.3 使用redis实现分布式锁

1. 多个客户端同时获取锁(setnx)只有一个会成功!
2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
3. 其他客户端等待重试
涉及的redis命令有:setnx和del!
2.3.1 编写代码
@Overridepublic void testLock() {// 1. 从redis中获取锁,setnxBoolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");if (lock) {// 查询redis中的num值String value = (String)this.redisTemplate.opsForValue().get("num");// 没有该值returnif (StringUtils.isBlank(value)){return ;}// 有值就转成成intint num = Integer.parseInt(value);// 把redis中的num值+1this.redisTemplate.opsForValue().set("num", String.valueOf(++num));// 2. 释放锁 delthis.redisTemplate.delete("lock");} else {// 3. 每隔1秒钟回调一次,再次尝试获取锁try {Thread.sleep(100);// 自旋testLock();} catch (InterruptedException e) {e.printStackTrace();}}}
重启,服务集群,通过网关压力测试:
[root@linux01 ~]# ab -n 5000 -c 100 http://192.168.200.1/admin/product/test/testLockThis is ApacheBench, Version 2.3 <$Revision: 1430300 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking 192.168.200.1 (be patient)Completed 500 requestsCompleted 1000 requestsCompleted 1500 requestsCompleted 2000 requestsCompleted 2500 requestsCompleted 3000 requestsCompleted 3500 requestsCompleted 4000 requestsCompleted 4500 requestsCompleted 5000 requestsFinished 5000 requestsServer Software:Server Hostname: 192.168.200.1Server Port: 80Document Path: /admin/product/test/testLockDocument Length: 64 bytesConcurrency Level: 100Time taken for tests: 120.433 secondsComplete requests: 5000Failed requests: 359(Connect: 0, Receive: 0, Length: 359, Exceptions: 0)Write errors: 0Total transferred: 1441795 bytesHTML transferred: 321795 bytesRequests per second: 41.52 [#/sec] (mean)Time per request: 2408.666 [ms] (mean)Time per request: 24.087 [ms] (mean, across all concurrent requests)Transfer rate: 11.69 [Kbytes/sec] receivedConnection Times (ms)min mean[+/-sd] median maxConnect: 0 2 10.5 1 514Processing: 4 2259 6029.3 20 64876Waiting: 4 2258 6029.0 19 64875Total: 4 2261 6031.0 21 64897Percentage of the requests served within a certain time (ms)50% 2166% 12175% 44480% 161090% 764295% 1550798% 2400899% 29537100% 64897 (longest request)[root@linux01 ~]#
查看redis
Connected.get最新商城项目:0>get num"5000"最新商城项目:0>
基本实现。
问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放!(其实就是在setnx命令执行后,并获得了锁,在执行del前,这段时间,如果有异常,则无法释放锁!)
解决:设置过期时间,自动释放锁。
2.3.2 优化之设置锁的过期时间
设置过期时间有两种方式:
- 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
2. 在set时指定过期时间(推荐)
设置过期时间:
压力测试肯定也没有问题。自行测试
问题:可能会释放其他服务器的锁。
场景:如果业务逻辑的执行时间是7s。执行流程如下
1. index1业务逻辑没执行完,3秒后锁被自动释放。
2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
3. index3获取到锁,执行业务逻辑
4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁, 导致index3的业务只执行1s就被别人释放。
最终等于没锁的情况。
解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
2.3.3 优化之UUID防误删


问题:删除操作缺乏原子性。
场景:
1. index1执行删除时,查询到的lock值确实和uuid相等
2. index1执行删除前,lock刚好过期时间已到,被redis自动释放在redis中没有了锁。
3. index2获取了lock,index2线程获取到了cpu的资源,开始执行方法
4. index1执行删除,此时会把index2的lock删除
index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行
删除的index2的锁!
2.3.4 优化之LUA脚本保证删除的原子性
@Overridepublic void testLock() {// 设置uuIdString uuid = UUID.randomUUID().toString();// 缓存的lock 对应的值 ,应该是index2 的uuidBoolean flag = redisTemplate.opsForValue().setIfAbsent("lock", uuid,1, TimeUnit.SECONDS);// 判断flag index=1if (flag){// 说明上锁成功! 执行业务逻辑String value = redisTemplate.opsForValue().get("num");// 判断if(StringUtils.isEmpty(value)){return;}// 进行数据转换int num = Integer.parseInt(value);// 放入缓存redisTemplate.opsForValue().set("num",String.valueOf(++num));// 定义一个lua 脚本String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 准备执行lua 脚本DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();// 将lua脚本放入DefaultRedisScript 对象中redisScript.setScriptText(script);// 设置DefaultRedisScript 这个对象的泛型redisScript.setResultType(Long.class);// 执行删除redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);}else {// 没有获取到锁!try {Thread.sleep(1000);// 睡醒了之后,重试testLock();} catch (InterruptedException e) {e.printStackTrace();}}}
Lua 脚本详解:参考:redis命令中文网http://doc.redisfans.com/string/set.html
完结:
redis + lua 实现分布式锁{单节点}
缺点:
lua在集群的情况下就不能保证删除的原子性了!为什么?暂时不清楚!答案见下面的redis集群状态下的问题:
2.3.5 redis实现分布式锁的总结
1、加锁
// 1. 从redis中获取锁,set k1 v1 px 20000 nxString uuid = UUID.randomUUID().toString();Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);
2、使用lua释放锁
// 2. 释放锁 delString script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 设置lua脚本返回的数据类型DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();// 设置lua脚本返回类型为LongredisScript.setResultType(Long.class);redisScript.setScriptText(script);redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
3、重试
Thread.sleep(500);testLock();
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 加锁和解锁必须具有原子性
redis集群状态下的问题:
1. 客户端A从master获取到锁
2. 在master将锁同步到slave之前,master宕掉了。
3. slave节点被晋级为master节点
4. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。
安全失效!
解决方案:了解即可!即redis集群情况下使用redlock解决!对于java,实现redlock是redisson。
2.4 使用redisson解决分布式锁
Github 地址:https://github.com/redisson/redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。(比如对于上面的加锁过程,我们不需要再,setex,del等等了,只需要调用redisson的api即可),我们这里重点关注redisson做分布式锁!其它功能先不了解!
官方文档地址:https://github.com/redisson/redisson/wiki
2.4.1 实现代码
1. 导入依赖 service-util<!-- redisson --><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.15.3</version></dependency>
/*** redisson配置信息*/@Data@Configuration@ConfigurationProperties("spring.redis")public class RedissonConfig {// 这些在product模块的dev配置中就有用到,以spring.redis开头的private String host;private String addresses;private String password;private String port;private int timeout = 3000;private int connectionPoolSize = 64;private int connectionMinimumIdleSize=10;private int pingConnectionInterval = 60000;private static String ADDRESS_PREFIX = "redis://";/*** 自动装配**/@BeanRedissonClient redissonSingle() {// 创建一个对象Config config = new Config();// 判断地址是否为空if(StringUtils.isEmpty(host)){throw new RuntimeException("host is empty");}// 配置服务,这里是以单节点,上线时用集群// config.useClusterServers().addNodeAddress().addNodeAddress()SingleServerConfig serverConfig = config.useSingleServer()//redis://127.0.0.1:7181.setAddress(ADDRESS_PREFIX + this.host + ":" + port).setTimeout(this.timeout).setPingConnectionInterval(pingConnectionInterval).setConnectionPoolSize(this.connectionPoolSize).setConnectionMinimumIdleSize(this.connectionMinimumIdleSize);// 判断是否需要密码 redis可以在配置文件中redis.conf中设置密码if(!StringUtils.isEmpty(this.password)) {serverConfig.setPassword(this.password);}// RedissonClient redisson = Redisson.create(config);return Redisson.create(config);}}
/*** @Description TODO*/@Servicepublic class TestServiceImpl implements TestService {@Autowiredprivate StringRedisTemplate redisTemplate;// 引入redisson@Autowiredprivate RedissonClient redissonClient;@Overridepublic void testLock() {RLock lock = redissonClient.getLock("lock");// 调用方法加锁lock.lock();// 业务逻辑 开始 ===========================================// 查询redis中的num值String value = (String)this.redisTemplate.opsForValue().get("num");// 没有该值returnif (StringUtils.isBlank(value)){return ;}// 有值就转成成intint num = Integer.parseInt(value);// 把redis中的num值+1this.redisTemplate.opsForValue().set("num", String.valueOf(++num));// 业务逻辑 结束 ===========================================// 解锁lock.unlock();}}
当然:为了无论业务异常与否都可以解锁,那么就用try包含业务逻辑
@Overridepublic void testLock() {RLock lock = redissonClient.getLock("lock");// 调用方法加锁lock.lock();try {// 业务逻辑 开始 ===========================================// 查询redis中的num值String value = (String)this.redisTemplate.opsForValue().get("num");// 没有该值returnif (StringUtils.isBlank(value)){return ;}// 有值就转成成intint num = Integer.parseInt(value);// 把redis中的num值+1this.redisTemplate.opsForValue().set("num", String.valueOf(++num));// 业务逻辑 结束 ===========================================} finally {// 解锁lock.unlock();}}
总结:redisson实现分布式锁
1.RLock lock = redissonClient.getLock(“lock”);
2.lock.lock(); lock.unlock(); // 这是最常的用redisson实现分布式锁的方式。
但是还有其它的,后面我们接着讲
2.4.2 可重入锁(Reentrant Lock)
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了(这即使宕机了,也可以解锁,不会死锁,应该是这么理解的吧??)。
快速入门使用的就是可重入锁。也是最常使用的锁。
最常见的使用:
RLock lock = redisson.getLock("anyLock");// 最常使用lock.lock();// 加锁以后10秒钟自动解锁// 无需调用unlock方法手动解锁lock.lock(10, TimeUnit.SECONDS);// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);if (res) {try {...} finally {lock.unlock();}}
2.4.3 读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");// 最常见的使用方法rwlock.readLock().lock();// 或rwlock.writeLock().lock();// 10秒钟以后自动解锁// 无需调用unlock方法手动解锁rwlock.readLock().lock(10, TimeUnit.SECONDS);// 或rwlock.writeLock().lock(10, TimeUnit.SECONDS);// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);// 或boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);...lock.unlock();
代码实现
TestController@GetMapping("read")public Result<String> read(){String msg = testService.readLock();return Result.ok(msg);}@GetMapping("write")public Result<String> write(){String msg = testService.writeLock();return Result.ok(msg);}
TestService接口String readLock();String writeLock();
实现类读锁,写锁要想达到互斥效果,那么锁的key ,必须是同一把 readwriteLock@Overridepublic String readLock() {// 初始化读写锁RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readwriteLock");RLock rLock = readWriteLock.readLock(); // 获取读锁rLock.lock(10, TimeUnit.SECONDS); // 加10s锁String msg = this.redisTemplate.opsForValue().get("msg");//rLock.unlock(); // 解锁return msg;}@Overridepublic String writeLock() {// 初始化读写锁RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readwriteLock");RLock rLock = readWriteLock.writeLock(); // 获取写锁rLock.lock(10, TimeUnit.SECONDS); // 加10s锁this.redisTemplate.opsForValue().set("msg", UUID.randomUUID().toString());//rLock.unlock(); // 解锁return "成功写入了内容。。。。。。";}
打开两个浏览器窗口测试:
http://localhost:8206/admin/product/test/read
http://localhost:8206/admin/product/test/write
- 同时访问写:一个写完之后,等待一会儿(约10s),另一个写开始
- 同时访问读:不用等待
- 先写后读:读要等待(约10s)写完成
- 先读后写:写要等待(约10s)读完成
c是最简单的,下来是b
6. 有关于缓存的常见问题:
a. 缓存击穿
是指缓存中的某一个key失效,如果出现高并发,则直接查询数据库,会导致系统崩溃!
解决方案:
加锁:分布式锁!
:基于redis 实现分布式锁!
b. 缓存穿透
是指用户查询一个在数据库中根本不存在的数据{数据库中没有该记录},那么我们做缓存的时候,不向缓存中放入数据的话!会导致缓存穿透。
set key value;
if(true){
// 返回数据
return redisDB;
}{
// 查询数据库 并将查询到的数据放入缓存!
SkuInfo skuInfo = getSkuInfo(skuId); skuId = 46; skuId = 146{直接查询的数据库!} 100万请求进来了!
if(skuInfo != null){
将skuInfo 放入缓存
setRedis(skuInfo);
}else{
// 将空的skuInfo 放入缓存
setRedis(skuInfo);
}
}
解决方案:
将null 放入缓存,同时这个key 的过期时间不能永久!设置一个相对较短时间,以分钟为单位就好。
c. 缓存雪崩
是指在同一时刻,缓存中的所有key全部失效!如果出现高并发,则直接查询数据库,会导致系统崩溃!
解决方案:
缓存的key,不要设置成同一个过期时间!
7. 本地锁局限性:
7.1 编写代码:
7.2 使用压力工具测试:
ab 压力测试:
ab -n 5000 -c 100 http://192.168.200.1:8206/admin/product/test/testLock
正确结果 num=5000,
但是,测试后发现:247 原因是代码没有添加锁! synchronized 添加到方法上!
结果正确!
7.3 开启多个服务!
server.port=8206
server.port=8216
server.port=8226
启动三个实例 ,此时需要访问网关,让网关进行负载均衡!
ab -n 5000 -c 100 http://192.168.200.1/admin/product/test/testLock
结果:在分布式项目中,本地锁失效!
1.4 缓存常见问题
缓存最常见的3个问题:面试
1. 缓存穿透
2. 缓存雪崩
3. 缓存击穿
缓存穿透: 是指查询一个不存在的数据,由于缓存无法命中,将去查询数据库,但是数据库也无此记录,并且出于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决:空结果也进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存雪崩:是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿: 是指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来之前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
与缓存雪崩的区别:
1. 击穿是一个热点key失效
2. 雪崩是很多key集体失效
解决:锁
2.3 使用redis实现分布式锁
本地锁的局限性

