Redis缓存及分布式锁
Redis缓存
为什么使用缓存
使用缓存带来的问题
缓存穿透
问题描述
解决方案
缓存雪崩
问题描述
解决方案
缓存击穿
问题描述
缓存击穿和缓存雪崩的区别
解决方案
JVM级别锁和分布式锁的关系
JVM级别锁(线程锁)
分布式锁
特点
Jedis的setnx和setex操作
使用setnx和setex实现分布式锁
编写代码并在单机环境下测试
模拟多机器环境并部署该服务
安装并启动nginx
windows下多个端口部署多个实例
遇到的小问题
使用压力测试进行测试
Redis缓存
为什么使用缓存
为了系统性能的提升,我们一般都会将数据放入缓存中,加速访问。而db承担数据落盘工作。
高并发系统,缓存是必备。
使用缓存带来的问题
缓存穿透
问题描述
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,并且处于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案
1、数据库中查询到值为空的话给redis中缓存空值
2、为了防止数据一致被null屏蔽了,我们给redis中缓存的所有数据都加上过期时间。某个时间后允许去数据库查新数据。
缓存雪崩
问题描述
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案
将redis中的key的过期时间设置为随机
缓存击穿
问题描述
针对redis中的某一个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
缓存击穿和缓存雪崩的区别
击穿是一个热点key失效
雪崩是很多key集体失效
解决方案
使用分布式锁
JVM级别锁和分布式锁的关系
JVM级别锁(线程锁)
在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就需要对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,专门提供了一些处理锁机制的一些API(synchronize/Lock等)。
分布式锁
分布式系统可能会有多份并且部署在不同的机器上,这些资源不是在线程之间共享了,而是属于进程之间共享的资源。因此,为了解决这个问题,我们必须要使用分布式锁的解决方案。
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
特点
排他性(在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取)
避免死锁(把锁在一段有限的时间之后,一定会被正常释放或者异常释放)
高可用(获取或释放锁的机制必须高可用且性能佳)。
Jedis的setnx和setex操作
1.引入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
</dependencies>
2.编写单元测试代码
@Test
public void testRedis(){
Jedis jedis=new Jedis("192.168.5.3",6379);
jedis.auth("123456");
SetParams setParams=new SetParams();
setParams.ex(60);
setParams.nx();
String s = UUID.randomUUID().toString();
//设置key为lock的value值为s,并设置了60s时间。
String lock = jedis.set("lock", s,setParams);
// jedis.setnx("lock", s);
// jedis.expire("lock",60);
System.out.println(lock);
}
3.运行结果如下:


使用setnx和setex实现分布式锁
编写代码并在单机环境下测试
1.引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.atguigu.locks</groupId>
<artifactId>locks-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>locks-test</name>
<description>测试锁的使用</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--内置tomcat-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.0</version>
</dependency>
</dependencies>
<!--打包-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.配置文件application.properties
spring.redis.host=192.168.1.104
spring.redis.password=123456
spring.redis.port=6379
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-idle=5
server.port=8089
3.appconfig配置
package com.example.locks.config;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
@Configuration
public class AppJedisConfig {
@Bean
public JedisPool jedisPoolConfig(RedisProperties properties) throws Exception {
//连接工厂中所有信息都有。
JedisPoolConfig config = new JedisPoolConfig();
RedisProperties.Pool pool = properties.getJedis().getPool();
//这些配置
config.setMaxIdle(pool.getMaxIdle());
config.setMaxTotal(pool.getMaxActive());
JedisPool jedisPool = null;
jedisPool = new JedisPool(config, properties.getHost(), properties.getPort(), Protocol.DEFAULT_TIMEOUT,properties.getPassword());
return jedisPool;
}
}
4.Controller代码
package com.example.locks.controller;
import com.example.locks.service.RedisIncrService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
RedisIncrService redisIncrService;
@GetMapping("/incr2")
public String incr2(){
redisIncrService.incrDistribute();
return "ok";
}
}
5.Service代码
@Service
public class RedisIncrService {
@Autowired
JedisPool jedisPool;
public void incrDistribute(){
/**
* setnx->set if not exist:原子操作。判断带保存。
* 1、获取和设置值必须是原子的
* 2、如果由于各种问题(未捕获的异常、断电等)导致锁没释放。其他人永远获取不到锁。加个过期时间。
* 3、加锁和加超时也必须是原子的。
* 4、业务逻辑超时,锁需要自动删除
* 5、删除锁需要保证原子性
**/
Jedis jedis = jedisPool.getResource();
try {
String token = UUID.randomUUID().toString();
String lock = jedis.set("lock", token, SetParams.setParams().ex(3).nx());
if(lock!=null&&lock.equalsIgnoreCase("OK")){
//ok
String num = jedis.get("num");
Integer i = Integer.parseInt(num);
i = i+1;
jedis.set("num",i.toString());
//删除锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList("lock"),Collections.singletonList(token));
System.out.println("删除锁ok....");
}else {
try {
Thread.sleep(1000);
incrDistribute();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
jedis.close();
}
}
6.启动服务,并在本地运行

模拟多机器环境并部署该服务

部署示意图如上。
安装并启动nginx
根据这位博主的文章安装成功
https://blog.csdn.net/qq_37345604/article/details/90034424
安装中学到的linux知识汇总
从本机的资源拷贝到其他linux机器
scp -r root/apps/lamp/nginx-1.13.9.tar.gz root@192.168.1.105:/root/apps/lamp
在编辑文本的时候使用如下指令可查看行号
:set nu

nginx.conf配置如下

windows下多个端口部署多个实例
java -jar locks-test-0.0.1-SNAPSHOT.jar --server.port=8080
java -jar locks-test-0.0.1-SNAPSHOT.jar --server.port=8081
java -jar locks-test-0.0.1-SNAPSHOT.jar --server.port=8082

在windows(1.100)机器下访问(1.104),如下图

遇到的小问题
部署过程中发现8082端口被占用
//查看已知的端口被哪个进程占用
netstat -ano|findstr "8082"
tasklist|findstr "9368"

使用压力测试进行测试

该功能使用特别简单,可以模拟多个线程批量同时访问







