设计方案我的微服务

Redis高级项目实战,都0202年了,还不会Redis?

2020-08-22  本文已影响0人  JAVA进阶之道

导读#

大家都听过1万小时定律,可事实真的是这样吗?做了1万小时的CRUD,不还只会CRUD吗,这年头适当的更新自身下技术栈出门和别人聊天吹牛的时候

面试专题#

什么是分布式锁?#

首先,为了确保分布式锁可用,至少要满足以下三个条件

  1. 互斥性。在任意时刻,只有一个客户端能持有锁
  2. 不会发生死锁。即便有一个客户端在持有锁的期间奔溃而没有主动解锁,也能保证后续其他客户端能加锁
  3. 解铃还须系铃人。加锁解锁必须是同一个客户端客户端自己不能把别人加的锁给解了

实现分布式锁方式?#

两种实现,下面都会有讲到

  1. 采用lua脚本操作分布式锁
  2. 采用setnx、setex命令连用的方式实现分布式锁

分布式锁的场景#

什么是分布式锁?#

为什么要有分布式锁?#

可以保证分布式部署的应用集群中,一个方法在同一操作只能被****一台机器上的一个线程执行

设计要求#

  1. 重入锁(避免死锁)
  2. 获取锁和释放高可用
  3. 获取锁和释放高性能

实现方案#

  1. 获取锁,使用setnx():SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1
  2. 若key存在,则什么都不做,返回【0】加锁,锁的value值为当前占有锁服务器内网IP编号拼接任务标识
  3. 释放锁候进行判断。并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁
  4. 返回1成功获取锁还设置一个获取超时时间,若超过这个时间放弃获取锁setex(key,value,expire)过期以秒为单位
  5. 释放锁的时候,判断是不是该(即value为当前服务器内网IP编号拼接任务标识),若是该锁,则执行delete进行锁释放

Redis分布式锁的实现#

创建一个SpringBoot工程#

网址:https://start.spring.io/

image

步骤#

1、启动类上加上注解@EnableScheduling

2、执行方法上加上注解@Scheduled

image

打包并上传至Linux服务器中启动#

准备3台Linux服务器,并将打好的jar包,上传至3台服务器中,然后启动

image

nohub之持久化启动方式 #

nohup java -jar jar名称 &

查看集群里面所有集群是否启动成功

1、先安装lsof:yum install lsof 2、验证:lsof -i:8080

TCP三次握手#

查看本机TCP连接状态#

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

为什么要三次握手?#

主要是为了防止已失效的连接请求报文段突然又传到了B,因而报文错乱问题,假定A发出的第一个连接请求报文段并没有丢失,而是在某些网络节点长时间滞留,一直延迟到连接释放以后的某个时间才到达B,本来这是一个早已经失效的报文段。但B收到失效的连接请求报文段后,就误认为是A又发出一个新的连接请求,于是就向A发出确认报文段,同意建立连接。

假定不采用三次握手,那么只要B发出确认新的连接就建立,这样一直等待A发来数据,B的许多资源就这样白白浪费了。

图解#

image

有3次握手了,为啥还有4次挥手?#

image

第一次挥手:主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不 会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但是,此时主动关闭方还可 以接受数据。
第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)。
第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。

作用#

确保数据能够完整传输

Redis分布式锁实现源码讲解#

图文讲解#

image

步骤#

  1. 分布式锁满足两个条件,一个是加有效时间,一个是高性能解锁
  2. 采用redis命令setnx (set if not exist)、setex(set expire value)实现
  3. 解锁流程不能遗漏否则导致任务执行一次就永不过期
  4. 加锁代码任务逻辑放到try catch代码块解锁流程finally代码块

项目结构#

pom.xml#

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);"><?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 https://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.3.3.RELEASE</version> <relativePath/> </parent> <groupId>com.cyb</groupId> <artifactId>yb-mobile-redis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>yb-mobile-redis</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project></pre>

application.properties#

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">spring.redis.database=0 spring.redis.host=192.168.199.142 spring.redis.port=6379 spring.redis.password=12345678 server.port=9001</pre>

RedisService.java#

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">package com.cyb.ybmobileredis.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.; import org.springframework.stereotype.Service; import java.io.Serializable; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /* * @ClassName:RedisService

LockNxExJob.java#

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">package com.cyb.ybmobileredis.schedule; import com.cyb.ybmobileredis.service.RedisService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.Enumeration; /** * @ClassName:LockNxExJob

验证#

打个jar运行在Linux上,一个在本地运行,一个获取锁成功一个获取锁失败

image

Redis分布式锁可能出现的问题#

image

上面我们已经使用代码实现分布式锁的功能,同一时刻****只能一把锁获取成功。从上图可以看出,极端情况下第一个Server获取锁成功后,服务或者Redis宕机了,会导致Redis锁无法释放的问题,其他Server一直获取锁失败

模拟server获取锁宕机#

先把项目跑起来获取锁之后,立马kill -9 进程id杀掉当前进程,然后在运行项目,控制台就会一直提示,获取锁失败了。

image

image

image

解决方案(重点)#

  1. 一次性执行一条命令就不会出现该情况发生,采用Lua脚本
  2. Redis从2.6之后,支持setnx、setex连用

lua脚本#

  1. 在redource目录下新增一个后缀名为.lua结尾的文件
  2. 编写lua脚本
  3. 传入lua脚本的key和arg
  4. 调用redisTemplate.execute方法执行脚本

编写lua脚本

image

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">local lockKey = KEYS[1] local lockValue = KEYS[2] -- setnx info local result_1 = redis.call('SETNX',lockKey,lockValue) if result_1 == true then local result_2 = redis.call('SETEX',lockKey,3600,lockValue) return result_1 else return result_1 end</pre>

封装调用lua脚本方法

image

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);"> @Autowired private RedisTemplate redisTemplate; private DefaultRedisScript<Boolean> lockScript; /** * 获取lua结果
*
* @param key 键
* @param value 值
* @return */ public Boolean luaExpress(String key, String value) { lockScript = new DefaultRedisScript<>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("add.lua")) ); //设置返回值 lockScript.setResultType(Boolean.class); //封装参数 List<Object> keyList = new ArrayList<>(); keyList.add(key); keyList.add(value); Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList); return result; }</pre>

改造之前的分布式锁方法

image

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">package com.cyb.ybmobileredis.schedule; import com.cyb.ybmobileredis.service.RedisService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Service; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; /** * @ClassName:LockNxExJob

验证#

image

补充解决Redis中的key乱码问题#

只需要添加RedisConfig.java配置文件即可

image

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">package com.cyb.ybmobileredis.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @ClassName:RedisConfig

RedisConnection实现分布式锁#

简介

RedisConnection实现分布式锁的方式,采用redisTemplate操作redisConnection实现setnx和setex两个命令连用

代码实现

image

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">package com.cyb.ybmobileredis.schedule; import com.cyb.ybmobileredis.service.RedisService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.types.Expiration; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Service; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; /** * @ClassName:LockNxExJob

测试#

image

分布式锁优化细节(重点)#

上面几个案例,已经实现了分布式锁的功能,但是极端情况下ServerA程序还没执行完ServerB程序执行完把锁释放掉了,就会造成A的锁释放掉了,这不是扯嘛,ServerA还没执行完,锁就被其他人释放了。解决方案:释放的时候,使用lua,通过get方法获取value,判断value是否等于本机ip,是自己的才能释放

image

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">package com.cyb.ybmobileredis.schedule; import com.cyb.ybmobileredis.service.RedisService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.types.Expiration; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Service; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; /** * @ClassName:LockNxExJob

unlock.lua脚本#

image

<pre class="prettyprint prettyprinted" style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: inherit !important; font-size: 12px !important; color: rgb(101, 108, 115);">local lockKey = KEYS[1] local lockValue = KEYS[2] -- get key local result_1 = redis.call('get', lockKey) if result_1 == lockValue then local result_2= redis.call('del', lockKey) return result_2 else return false end</pre>

演示#

为了演示方便,我把失效时间设置短一点,8秒

image

尾声#

嫖都嫖完了,难道你忍心不点赞关注嘛,O(∩_∩)O哈哈~~~~~今天,先到这,后续继续写Redis秒杀系统的设计

上一篇 下一篇

猜你喜欢

热点阅读