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中添加测试方法

  1. /**
  2. * @Description TODO
  3. */
  4. @Api(tags = "测试接口")
  5. @RestController
  6. @RequestMapping("admin/product/test")
  7. public class TestController {
  8. @Autowired
  9. private TestService testService;
  10. @GetMapping("testLock")
  11. public Result testLock() {
  12. testService.testLock();
  13. return Result.ok();
  14. }
  15. }

接口及实现类

/
1. 在缓存中存储一个num 初始值为 0
2. 利用缓存的StringRedisTemplate 获取到的当前的num 值
3. 如果num 不为空,则需要对当前值 进行 + 1 操作
4. 如果num 为空,则返回即可!
*/
这个是实现类中需要做的事情,其中第一步:是在redis中设置num = 0,即set num = 0;

  1. public interface TestService {
  2. void testLock();
  3. }
  4. @Service
  5. public class TestServiceImpl implements TestService {
  6. @Autowired
  7. private StringRedisTemplate redisTemplate;
  8. /*
  9. * 1. 在缓存中存储一个num 初始值为 0
  10. * 2. 利用缓存的StringRedisTemplate 获取到的当前的num 值
  11. * 3. 如果num 不为空,则需要对当前值 进行 + 1 操作
  12. * 4. 如果num 为空,则返回即可!
  13. * */
  14. @Override
  15. public void testLock() {
  16. // 查询redis中的num值
  17. String value = (String)this.redisTemplate.opsForValue().get("num");
  18. // 没有该值return
  19. if (StringUtils.isBlank(value)){
  20. return ;
  21. }
  22. // 有值就转成成int
  23. int num = Integer.parseInt(value);
  24. // 把redis中的num值+1
  25. this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
  26. }
  27. }
  28. }

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执行上述命令测试。
测试结果:

  1. [root@linux01 ~]# ab -n 5000 -c 100 http://192.168.200.1:8206/admin/product/test/testLock
  2. This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
  3. Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
  4. Licensed to The Apache Software Foundation, http://www.apache.org/
  5. Benchmarking 192.168.200.1 (be patient)
  6. Completed 500 requests
  7. Completed 1000 requests
  8. Completed 1500 requests
  9. Completed 2000 requests
  10. Completed 2500 requests
  11. Completed 3000 requests
  12. Completed 3500 requests
  13. Completed 4000 requests
  14. Completed 4500 requests
  15. Completed 5000 requests
  16. Finished 5000 requests
  17. Server Software:
  18. Server Hostname: 192.168.200.1
  19. Server Port: 8206
  20. Document Path: /admin/product/test/testLock
  21. Document Length: 53 bytes
  22. Concurrency Level: 100
  23. Time taken for tests: 16.606 seconds
  24. Complete requests: 5000
  25. Failed requests: 0
  26. Write errors: 0
  27. Total transferred: 790000 bytes
  28. HTML transferred: 265000 bytes
  29. Requests per second: 301.09 [#/sec] (mean)
  30. Time per request: 332.121 [ms] (mean)
  31. Time per request: 3.321 [ms] (mean, across all concurrent requests)
  32. Transfer rate: 46.46 [Kbytes/sec] received
  33. Connection Times (ms)
  34. min mean[+/-sd] median max
  35. Connect: 1 79 67.0 61 407
  36. Processing: 17 179 128.3 143 3757
  37. Waiting: 11 131 106.8 95 3566
  38. Total: 59 258 151.3 223 3759
  39. Percentage of the requests served within a certain time (ms)
  40. 50% 223
  41. 66% 298
  42. 75% 337
  43. 80% 362
  44. 90% 451
  45. 95% 565
  46. 98% 644
  47. 99% 698
  48. 100% 3759 (longest request)
  49. [root@linux01 ~]#

查看redis值:显然是由并发问题的!

  1. List of unsupported commands: DUMP, RESTORE, AUTH
  2. Connecting ...
  3. Connected.
  4. 最新商城项目:0>set num 0
  5. "OK"
  6. 最新商城项目:0>get num
  7. "326"
  8. 最新商城项目:0>

2.1.3 使用本地锁

很简单,在方法上加锁即可!

  1. @Service
  2. public class TestServiceImpl implements TestService {
  3. @Autowired
  4. private StringRedisTemplate redisTemplate;
  5. /*
  6. * 1. 在缓存中存储一个num 初始值为 0
  7. * 2. 利用缓存的StringRedisTemplate 获取到的当前的num 值
  8. * 3. 如果num 不为空,则需要对当前值 进行 + 1 操作
  9. * 4. 如果num 为空,则返回即可!
  10. * */
  11. @Override
  12. public synchronized void testLock() {
  13. // 查询redis中的num值
  14. String value = (String)this.redisTemplate.opsForValue().get("num");
  15. // 没有该值return
  16. if (StringUtils.isBlank(value)){
  17. return ;
  18. }
  19. // 有值就转成成int
  20. int num = Integer.parseInt(value);
  21. // 把redis中的num值+1
  22. this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
  23. }
  24. }

依然将redis中的num设置为0.
再次压测:测试就是正确的!

  1. 最新商城项目:0>set num 0
  2. "OK"
  3. 最新商城项目:0>get num
  4. "5000"
  5. 最新商城项目:0>

完美!与预期一致,是否真的完美?
接下来再看集群情况下,会怎样?

2.1.4 本地锁问题演示锁

接下来启动8206 8216 8226 三个运行实例。
运行多个service-product实例:
server.port=8216
server.port=8226
运行方法演示一个:
image.png
image.png
注意:同时还要启动网关,这样才能访问这个集群!同时num设置为0!
通过网关压力测试
启动网关:
ab -n 5000 -c 100 http://192.168.200.1/admin/product/test/testLock

  1. [root@linux01 ~]# ab -n 5000 -c 100 http://192.168.200.1/admin/product/test/testLock
  2. This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
  3. Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
  4. Licensed to The Apache Software Foundation, http://www.apache.org/
  5. Benchmarking 192.168.200.1 (be patient)
  6. Completed 500 requests
  7. Completed 1000 requests
  8. Completed 1500 requests
  9. Completed 2000 requests
  10. Completed 2500 requests
  11. Completed 3000 requests
  12. Completed 3500 requests
  13. Completed 4000 requests
  14. Completed 4500 requests
  15. Completed 5000 requests
  16. Finished 5000 requests
  17. Server Software:
  18. Server Hostname: 192.168.200.1
  19. Server Port: 80
  20. Document Path: /admin/product/test/testLock
  21. Document Length: 64 bytes
  22. Concurrency Level: 100
  23. Time taken for tests: 52.790 seconds
  24. Complete requests: 5000
  25. Failed requests: 168
  26. (Connect: 0, Receive: 0, Length: 168, Exceptions: 0)
  27. Write errors: 0
  28. Total transferred: 1440840 bytes
  29. HTML transferred: 320840 bytes
  30. Requests per second: 94.71 [#/sec] (mean)
  31. Time per request: 1055.804 [ms] (mean)
  32. Time per request: 10.558 [ms] (mean, across all concurrent requests)
  33. Transfer rate: 26.65 [Kbytes/sec] received
  34. Connection Times (ms)
  35. min mean[+/-sd] median max
  36. Connect: 0 10 42.1 1 413
  37. Processing: 7 749 1140.0 390 15340
  38. Waiting: 0 746 1136.7 387 14720
  39. Total: 7 759 1161.3 403 15342
  40. Percentage of the requests served within a certain time (ms)
  41. 50% 403
  42. 66% 725
  43. 75% 947
  44. 80% 1113
  45. 90% 1629
  46. 95% 2425
  47. 98% 4257
  48. 99% 7047
  49. 100% 15342 (longest request)
  50. [root@linux01 ~]#

查看redis:

  1. Connected.
  2. 最新商城项目:0>get num
  3. "2370"
  4. 最新商城项目:0>

集群情况下又出问题了!!!
以上测试,可以发现:
本地锁只能锁住同一工程内的资源,在分布式系统里面都存在局限性。
此时需要分布式锁。。

2.2 分布式锁实现的解决方案

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁
  2. 基于缓存(Redis等)
  3. 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点:
1. 性能:redis最高
2. 可靠性:zookeeper最高
这里,我们就基于redis实现分布式锁。如果是银行业务,可能需要用zookeeper,安全性更高,但是性能略差!

2.3 使用redis实现分布式锁

image.png
1. 多个客户端同时获取锁(setnx)只有一个会成功!
2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
3. 其他客户端等待重试
涉及的redis命令有:setnx和del!

2.3.1 编写代码

  1. @Override
  2. public void testLock() {
  3. // 1. 从redis中获取锁,setnx
  4. Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");
  5. if (lock) {
  6. // 查询redis中的num值
  7. String value = (String)this.redisTemplate.opsForValue().get("num");
  8. // 没有该值return
  9. if (StringUtils.isBlank(value)){
  10. return ;
  11. }
  12. // 有值就转成成int
  13. int num = Integer.parseInt(value);
  14. // 把redis中的num值+1
  15. this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
  16. // 2. 释放锁 del
  17. this.redisTemplate.delete("lock");
  18. } else {
  19. // 3. 每隔1秒钟回调一次,再次尝试获取锁
  20. try {
  21. Thread.sleep(100);
  22. // 自旋
  23. testLock();
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }

重启,服务集群,通过网关压力测试:

  1. [root@linux01 ~]# ab -n 5000 -c 100 http://192.168.200.1/admin/product/test/testLock
  2. This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
  3. Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
  4. Licensed to The Apache Software Foundation, http://www.apache.org/
  5. Benchmarking 192.168.200.1 (be patient)
  6. Completed 500 requests
  7. Completed 1000 requests
  8. Completed 1500 requests
  9. Completed 2000 requests
  10. Completed 2500 requests
  11. Completed 3000 requests
  12. Completed 3500 requests
  13. Completed 4000 requests
  14. Completed 4500 requests
  15. Completed 5000 requests
  16. Finished 5000 requests
  17. Server Software:
  18. Server Hostname: 192.168.200.1
  19. Server Port: 80
  20. Document Path: /admin/product/test/testLock
  21. Document Length: 64 bytes
  22. Concurrency Level: 100
  23. Time taken for tests: 120.433 seconds
  24. Complete requests: 5000
  25. Failed requests: 359
  26. (Connect: 0, Receive: 0, Length: 359, Exceptions: 0)
  27. Write errors: 0
  28. Total transferred: 1441795 bytes
  29. HTML transferred: 321795 bytes
  30. Requests per second: 41.52 [#/sec] (mean)
  31. Time per request: 2408.666 [ms] (mean)
  32. Time per request: 24.087 [ms] (mean, across all concurrent requests)
  33. Transfer rate: 11.69 [Kbytes/sec] received
  34. Connection Times (ms)
  35. min mean[+/-sd] median max
  36. Connect: 0 2 10.5 1 514
  37. Processing: 4 2259 6029.3 20 64876
  38. Waiting: 4 2258 6029.0 19 64875
  39. Total: 4 2261 6031.0 21 64897
  40. Percentage of the requests served within a certain time (ms)
  41. 50% 21
  42. 66% 121
  43. 75% 444
  44. 80% 1610
  45. 90% 7642
  46. 95% 15507
  47. 98% 24008
  48. 99% 29537
  49. 100% 64897 (longest request)
  50. [root@linux01 ~]#

查看redis

  1. Connected.get
  2. 最新商城项目:0>get num
  3. "5000"
  4. 最新商城项目:0>

基本实现。
问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放!(其实就是在setnx命令执行后,并获得了锁,在执行del前,这段时间,如果有异常,则无法释放锁!)
解决:设置过期时间,自动释放锁

2.3.2 优化之设置锁的过期时间

设置过期时间有两种方式:

  1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
    2. 在set时指定过期时间(推荐)
    image.png
    设置过期时间:
    image.png
    压力测试肯定也没有问题。自行测试
    问题:可能会释放其他服务器的锁

场景:如果业务逻辑的执行时间是7s。执行流程如下
1. index1业务逻辑没执行完,3秒后锁被自动释放。
2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
3. index3获取到锁,执行业务逻辑
4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁, 导致index3的业务只执行1s就被别人释放。
最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

2.3.3 优化之UUID防误删

image.png
image.png
问题:删除操作缺乏原子性。

场景:
1. index1执行删除时,查询到的lock值确实和uuid相等
image.png
2. index1执行删除前,lock刚好过期时间已到,被redis自动释放在redis中没有了锁。
image.png
3. index2获取了lock,index2线程获取到了cpu的资源,开始执行方法
4. index1执行删除,此时会把index2的lock删除

index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行
image.png
删除的index2的锁!

2.3.4 优化之LUA脚本保证删除的原子性

  1. @Override
  2. public void testLock() {
  3. // 设置uuId
  4. String uuid = UUID.randomUUID().toString();
  5. // 缓存的lock 对应的值 ,应该是index2 的uuid
  6. Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", uuid,1, TimeUnit.SECONDS);
  7. // 判断flag index=1
  8. if (flag){
  9. // 说明上锁成功! 执行业务逻辑
  10. String value = redisTemplate.opsForValue().get("num");
  11. // 判断
  12. if(StringUtils.isEmpty(value)){
  13. return;
  14. }
  15. // 进行数据转换
  16. int num = Integer.parseInt(value);
  17. // 放入缓存
  18. redisTemplate.opsForValue().set("num",String.valueOf(++num));
  19. // 定义一个lua 脚本
  20. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  21. // 准备执行lua 脚本
  22. DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
  23. // 将lua脚本放入DefaultRedisScript 对象中
  24. redisScript.setScriptText(script);
  25. // 设置DefaultRedisScript 这个对象的泛型
  26. redisScript.setResultType(Long.class);
  27. // 执行删除
  28. redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
  29. }else {
  30. // 没有获取到锁!
  31. try {
  32. Thread.sleep(1000);
  33. // 睡醒了之后,重试
  34. testLock();
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. }

Lua 脚本详解:参考:redis命令中文网http://doc.redisfans.com/string/set.html
image.png
完结:
redis + lua 实现分布式锁{单节点}
缺点:
lua在集群的情况下就不能保证删除的原子性了!为什么?暂时不清楚!答案见下面的redis集群状态下的问题

2.3.5 redis实现分布式锁的总结

1、加锁

  1. // 1. 从redis中获取锁,set k1 v1 px 20000 nx
  2. String uuid = UUID.randomUUID().toString();
  3. Boolean lock = this.redisTemplate.opsForValue()
  4. .setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);

2、使用lua释放锁

  1. // 2. 释放锁 del
  2. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  3. // 设置lua脚本返回的数据类型
  4. DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
  5. // 设置lua脚本返回类型为Long
  6. redisScript.setResultType(Long.class);
  7. redisScript.setScriptText(script);
  8. redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);

3、重试

  1. Thread.sleep(500);
  2. testLock();

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 加锁和解锁必须具有原子性

redis集群状态下的问题
1. 客户端A从master获取到锁
2. 在master将锁同步到slave之前,master宕掉了。
3. slave节点被晋级为master节点
4. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。
安全失效!
解决方案:了解即可!即redis集群情况下使用redlock解决!对于java,实现redlock是redisson。
image.png

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做分布式锁!其它功能先不了解!
image.png
官方文档地址:https://github.com/redisson/redisson/wiki

2.4.1 实现代码

  1. 1. 导入依赖 service-util
  2. <!-- redisson -->
  3. <dependency>
  4. <groupId>org.redisson</groupId>
  5. <artifactId>redisson</artifactId>
  6. <version>3.15.3</version>
  7. </dependency>
  1. /**
  2. * redisson配置信息
  3. */
  4. @Data
  5. @Configuration
  6. @ConfigurationProperties("spring.redis")
  7. public class RedissonConfig {
  8. // 这些在product模块的dev配置中就有用到,以spring.redis开头的
  9. private String host;
  10. private String addresses;
  11. private String password;
  12. private String port;
  13. private int timeout = 3000;
  14. private int connectionPoolSize = 64;
  15. private int connectionMinimumIdleSize=10;
  16. private int pingConnectionInterval = 60000;
  17. private static String ADDRESS_PREFIX = "redis://";
  18. /**
  19. * 自动装配
  20. *
  21. */
  22. @Bean
  23. RedissonClient redissonSingle() {
  24. // 创建一个对象
  25. Config config = new Config();
  26. // 判断地址是否为空
  27. if(StringUtils.isEmpty(host)){
  28. throw new RuntimeException("host is empty");
  29. }
  30. // 配置服务,这里是以单节点,上线时用集群
  31. // config.useClusterServers().addNodeAddress().addNodeAddress()
  32. SingleServerConfig serverConfig = config.useSingleServer()
  33. //redis://127.0.0.1:7181
  34. .setAddress(ADDRESS_PREFIX + this.host + ":" + port)
  35. .setTimeout(this.timeout)
  36. .setPingConnectionInterval(pingConnectionInterval)
  37. .setConnectionPoolSize(this.connectionPoolSize)
  38. .setConnectionMinimumIdleSize(this.connectionMinimumIdleSize);
  39. // 判断是否需要密码 redis可以在配置文件中redis.conf中设置密码
  40. if(!StringUtils.isEmpty(this.password)) {
  41. serverConfig.setPassword(this.password);
  42. }
  43. // RedissonClient redisson = Redisson.create(config);
  44. return Redisson.create(config);
  45. }
  46. }
  1. /**
  2. * @Description TODO
  3. */
  4. @Service
  5. public class TestServiceImpl implements TestService {
  6. @Autowired
  7. private StringRedisTemplate redisTemplate;
  8. // 引入redisson
  9. @Autowired
  10. private RedissonClient redissonClient;
  11. @Override
  12. public void testLock() {
  13. RLock lock = redissonClient.getLock("lock");
  14. // 调用方法加锁
  15. lock.lock();
  16. // 业务逻辑 开始 ===========================================
  17. // 查询redis中的num值
  18. String value = (String)this.redisTemplate.opsForValue().get("num");
  19. // 没有该值return
  20. if (StringUtils.isBlank(value)){
  21. return ;
  22. }
  23. // 有值就转成成int
  24. int num = Integer.parseInt(value);
  25. // 把redis中的num值+1
  26. this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
  27. // 业务逻辑 结束 ===========================================
  28. // 解锁
  29. lock.unlock();
  30. }
  31. }

当然:为了无论业务异常与否都可以解锁,那么就用try包含业务逻辑

  1. @Override
  2. public void testLock() {
  3. RLock lock = redissonClient.getLock("lock");
  4. // 调用方法加锁
  5. lock.lock();
  6. try {
  7. // 业务逻辑 开始 ===========================================
  8. // 查询redis中的num值
  9. String value = (String)this.redisTemplate.opsForValue().get("num");
  10. // 没有该值return
  11. if (StringUtils.isBlank(value)){
  12. return ;
  13. }
  14. // 有值就转成成int
  15. int num = Integer.parseInt(value);
  16. // 把redis中的num值+1
  17. this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
  18. // 业务逻辑 结束 ===========================================
  19. } finally {
  20. // 解锁
  21. lock.unlock();
  22. }
  23. }

总结: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的参数来指定加锁的时间。超过这个时间后锁便自动解开了(这即使宕机了,也可以解锁,不会死锁,应该是这么理解的吧??)。
快速入门使用的就是可重入锁。也是最常使用的锁。
最常见的使用:

  1. RLock lock = redisson.getLock("anyLock");
  2. // 最常使用
  3. lock.lock();
  4. // 加锁以后10秒钟自动解锁
  5. // 无需调用unlock方法手动解锁
  6. lock.lock(10, TimeUnit.SECONDS);
  7. // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
  8. boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
  9. if (res) {
  10. try {
  11. ...
  12. } finally {
  13. lock.unlock();
  14. }
  15. }

改造程序:
image.png
重启后在浏览器测试:

2.4.3 读写锁(ReadWriteLock)

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

  1. RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
  2. // 最常见的使用方法
  3. rwlock.readLock().lock();
  4. // 或
  5. rwlock.writeLock().lock();
  6. // 10秒钟以后自动解锁
  7. // 无需调用unlock方法手动解锁
  8. rwlock.readLock().lock(10, TimeUnit.SECONDS);
  9. // 或
  10. rwlock.writeLock().lock(10, TimeUnit.SECONDS);
  11. // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
  12. boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
  13. // 或
  14. boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
  15. ...
  16. lock.unlock();

代码实现

  1. TestController
  2. @GetMapping("read")
  3. public Result<String> read(){
  4. String msg = testService.readLock();
  5. return Result.ok(msg);
  6. }
  7. @GetMapping("write")
  8. public Result<String> write(){
  9. String msg = testService.writeLock();
  10. return Result.ok(msg);
  11. }
  1. TestService接口
  2. String readLock();
  3. String writeLock();
  1. 实现类
  2. 读锁,写锁要想达到互斥效果,那么锁的key ,必须是同一把 readwriteLock
  3. @Override
  4. public String readLock() {
  5. // 初始化读写锁
  6. RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readwriteLock");
  7. RLock rLock = readWriteLock.readLock(); // 获取读锁
  8. rLock.lock(10, TimeUnit.SECONDS); // 加10s锁
  9. String msg = this.redisTemplate.opsForValue().get("msg");
  10. //rLock.unlock(); // 解锁
  11. return msg;
  12. }
  13. @Override
  14. public String writeLock() {
  15. // 初始化读写锁
  16. RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readwriteLock");
  17. RLock rLock = readWriteLock.writeLock(); // 获取写锁
  18. rLock.lock(10, TimeUnit.SECONDS); // 加10s锁
  19. this.redisTemplate.opsForValue().set("msg", UUID.randomUUID().toString());
  20. //rLock.unlock(); // 解锁
  21. return "成功写入了内容。。。。。。";
  22. }

打开两个浏览器窗口测试:
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实现分布式锁

本地锁的局限性