专业搭建网站公司,下载优化大师app,浙江建设网查询,企业文化墙设计公司Redlock红锁算法
官网地址#xff1a;Distributed Locks with Redis | Redis 为什么要使用RedLock#xff1f; 解释#xff1a; 线程 1 首先获取锁成功#xff0c;将键值对写入 redis 的 master 节点#xff0c;在 redis 将该键值对同步到 slave 节点之前#xff0c;mas…Redlock红锁算法
官网地址Distributed Locks with Redis | Redis 为什么要使用RedLock 解释 线程 1 首先获取锁成功将键值对写入 redis 的 master 节点在 redis 将该键值对同步到 slave 节点之前master 发生了故障redis 触发故障转移其中一个 slave 升级为新的 master此时新上位的master并不包含线程1写入的键值对因此线程 2 尝试获取锁也可以成功拿到锁此时相当于有两个线程获取到了锁可能会导致各种预期之外的情况发生例如最常见的脏数据。 我们加的是排它独占锁同一时间只能有一个建redis锁成功并持有锁严禁出现2个以上的请求线程拿到锁。危险的。
RedLock算法设计理念
Redis也提供了Redlock算法用来实现基于多个实例的分布式锁。
锁变量由多个实例维护即使有实例发生了故障锁变量仍然是存在的客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案可以在实际开发中使用。最下方还有笔记。 翻译 设计理念
该方案也是基于set 加锁、Lua 脚本解锁进行改良的所以redis之父antirez 只描述了差异的地方大致方案如下 假设我们有N个Redis主节点例如 N 5这些节点是完全独立的我们不使用复制或任何其他隐式协调系统为了取到锁客户端执行以下操作
获取当前时间以毫秒为单位依次尝试从5个实例使用相同的 key 和随机值例如 UUID获取锁。当向Redis 请求获取锁时客户端应该设置一个超时时间这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用客户端应该尽快尝试去另外一个 Redis 实例请求获取锁客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数N/21这里是 3 个节点的 Redis 节点都取到锁并且获取锁使用的时间小于锁失效时间时锁才算获取成功如果取到了锁其真正有效时间等于初始有效时间减去获取锁所使用的时间步骤 3 计算的结果。如果由于某些原因未能获得锁无法在至少 N/2 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间客户端应该在所有的 Redis 实例上进行解锁即便某些Redis实例根本就没有加锁成功防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁。
该方案为了解决数据不一致的问题直接舍弃了异步复制只使用 master 节点同时由于舍弃了 slave为了保证可用性引入了 N 个节点官方建议是 5。
客户端只有在满足下面的这两个条件时才能认为是加锁成功 条件1客户端从超过半数大于等于N/21的Redis实例上成功获取到了锁 条件2客户端获取锁的总耗时没有超过锁的有效时间。 解决方案 为什么是奇数 N 2X 1 (N是最终部署机器数X是容错机器数)
1、先知道什么是容错
失败了多少个机器实例后我还是可以容忍的所谓的容忍就是数据一致性还是可以Ok的CP数据一致性还是可以满足。
加入在集群环境中redis失败1台可接受。2X1 2 * 11 3部署3台死了1个剩下2个可以正常工作那就部署3台。
加入在集群环境中redis失败2台可接受。2X1 2 * 21 5部署5台死了2个剩下3个可以正常工作那就部署5台。
2、为什么是奇数
最少的机器最多的产出效果
加入在集群环境中redis失败1台可接受。2N2 2 * 12 4部署4台
加入在集群环境中redis失败2台可接受。2N2 2 * 22 6部署6台
Redisson
Redisson就是RedLock的实现。
Redisson是java的redis客户端之一提供了一些api方便操作redis。
Redisson官网Redisson: Easy Redis Java client with features of In-Memory Data Grid
Redisson之Githubhttps://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
Redisson之解缺分布式锁https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers Redisson使用
根据之前案例V8.0版本进行改造详细可查阅Redis 分布式锁_Please Sit Down的博客-CSDN博客
如何使用
官网81-可重入锁reentrant-lock V9.0 版本单机版
pom.xml
!--redisson--
dependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.13.4/version
/dependency
config配置
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;Configuration
public class RedisConfig {Beanpublic RedisTemplateString, Object redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {RedisTemplateString,Object redisTemplate new RedisTemplate();redisTemplate.setConnectionFactory(lettuceConnectionFactory);//设置key序列化方式stringredisTemplate.setKeySerializer(new StringRedisSerializer());//设置value的序列化方式jsonredisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}//单Redis节点模式Beanpublic Redisson redisson() {Config config new Config();config.useSingleServer().setAddress(redis://192.168.111.175:6379).setDatabase(0).setPassword(111111);return (Redisson) Redisson.create(config);}
}
controller
import com.atguigu.redislock.service.InventoryService;
import com.atguigu.redislock.service.InventoryService2;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;RestController
Api(tags redis分布式锁测试)
public class InventoryController {Autowiredprivate InventoryService inventoryService;ApiOperation(扣减库存saleByRedisson一次卖一个)GetMapping(value /inventory/saleByRedisson)public String saleByRedisson() {return inventoryService.saleByRedisson();}
}service
import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
import com.atguigu.redislock.mylock.RedisDistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;Service
Slf4j
public class InventoryService {Autowiredprivate StringRedisTemplate stringRedisTemplate;Value(${server.port})private String port;Autowiredprivate DistributedLockFactory distributedLockFactory;Autowiredprivate Redisson redisson;public String saleByRedisson() {String retMessage ;String key zzyyRedisLock;RLock redissonLock redisson.getLock(key);redissonLock.lock();try {//1 查询库存信息String result stringRedisTemplate.opsForValue().get(inventory001);//2 判断库存是否足够Integer inventoryNumber result null ? 0 : Integer.parseInt(result);//3 扣减库存if(inventoryNumber 0) {stringRedisTemplate.opsForValue().set(inventory001,String.valueOf(--inventoryNumber));retMessage 成功卖出一个商品库存剩余: inventoryNumber;System.out.println(retMessage);}else{retMessage 商品卖完了o(╥﹏╥)o;}}finally {redissonLock.unlock();}return retMessage\t服务端口号port;}
}
测试
使用Jmeter出现BUG V9.1 版本单机版
解锁操作需要判断是否为自己的锁不能解其他人的锁。这里使用if(redissonLock.isLocked() redissonLock.isHeldByCurrentThread()) 判断Redisson自动会给我们判断是否为自己的锁。 import cn.hutool.core.util.IdUtil;import com.atguigu.redislock.mylock.DistributedLockFactory;import com.atguigu.redislock.mylock.RedisDistributedLock;import lombok.extern.slf4j.Slf4j;import org.redisson.Redisson;import org.redisson.api.RLock;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.stereotype.Service;import java.util.Arrays;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;Service Slf4jpublic class InventoryService { Autowired private StringRedisTemplate stringRedisTemplate; Value(${server.port}) private String port; Autowired private DistributedLockFactory distributedLockFactory; Autowired private Redisson redisson; public String saleByRedisson() { String retMessage ; String key zzyyRedisLock; RLock redissonLock redisson.getLock(key); redissonLock.lock(); try { //1 查询库存信息 String result stringRedisTemplate.opsForValue().get(inventory001); //2 判断库存是否足够 Integer inventoryNumber result null ? 0 : Integer.parseInt(result); //3 扣减库存 if(inventoryNumber 0) { stringRedisTemplate.opsForValue().set(inventory001,String.valueOf(--inventoryNumber)); retMessage 成功卖出一个商品库存剩余: inventoryNumber; System.out.println(retMessage); }else{ retMessage 商品卖完了o(╥﹏╥)o; } }finally { if(redissonLock.isLocked() redissonLock.isHeldByCurrentThread()) { redissonLock.unlock(); } } return retMessage\t服务端口号port; } } Redisson源码解析
Redisson也实现了之前版本中的缓存续命功能Redisson中是使用守护线程进行续命。 额外起一个线程定期检查线程是否还持有锁如果有则延长过期时间。 Redisson 里面就实现了这个方案使用“看门狗”定期检查每1/3的锁时间检查1次如果线程还持有锁则刷新过期时间 在获取锁成功后给锁加一个watchdogwatchdog会起一个定时任务在锁没有被释放且快要过期的时候会续期。 源码1通过redisson新建出来的锁key默认是30秒 源码2
源码位置RedissonLock.java - lock() - tryAcquire() - tryAcquireAsync() 源码3 流程解释 1、通过exists判断如果锁不存在则设置值和过明时间加锁成功 2、通过hexists判断如果锁已存在并目锁的是当前线程则证明是重入锁加锁成功 3、如果锁已存在但锁的不是当前线程则证明有其他线程持有锁。返回当前锁的过期时间代表了锁key的剩余生存时间加锁失败 源码4 这里面初始化了一个定时器dely 的时间是 internalLockLeaseTime/3。在 Redisson 中internalLockLeaseTime 是 30s也就是每隔 10s 续期一次每次 30s。 解释 watch dog自动延期机制 客户端A加锁成功就会启动一个watch dog看门狗他是一个后台线程会每隔10秒检查一下如果客户端A还持有锁key那么就会不断的延长锁key的生存时间默认每次续命又从30秒新开始。 自动续期lua脚本分析 源码5
解锁lua脚本解释 多机使用Redisson案例
理论来源 这个锁的算法实现了多redis实例的情况相对于单redis节点来说优点在于 防止了 单节点故障造成整个服务停止运行的情况且在多节点中锁的设计及多节点同时崩溃等各种意外情况有自己独特的设计方法。
Redisson 分布式锁支持 MultiLock 机制可以将多个锁合并为一个大锁对一个大锁进行统一的申请加锁以及释放锁。
最低保证分布式锁的有效性及安全性的要求如下 1.互斥任何时刻只能有一个client获取锁 2.释放死锁即使锁定资源的服务崩溃或者分区仍然能释放锁 3.容错性只要多数redis节点一半以上在使用client就可以获取和释放锁 网上讲的基于故障转移实现的redis主从无法真正实现Redlock
因为redis在进行主从复制时是异步完成的比如在clientA获取锁后主redis复制数据到从redis过程中崩溃了导致没有复制到从redis中然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁这是clientB尝试获取锁并且能够成功获取锁导致互斥失效
代码来源
https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers
备注目前RedLock已被遗弃换用MultiLock 案例
1、启动3台Redis主机实例都是主节点使用docker docker run -p 6381:6379 --name redis-master-1 -d redis docker run -p 6382:6379 --name redis-master-2 -d redis docker run -p 6383:6379 --name redis-master-3 -d redis 进入实例命令 docker exec -it redis-master-1 /bin/bash 或者 docker exec -it redis-master-1 redis-cli docker exec -it redis-master-2 /bin/bash 或者 docker exec -it redis-master-2 redis-cli docker exec -it redis-master-3 /bin/bash 或者 docker exec -it redis-master-3 redis-cli 2、pom.xml
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.3.10.RELEASE/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.atguigu.redis.redlock/groupIdartifactIdredis_redlock/artifactIdversion0.0.1-SNAPSHOT/versionpropertiesjava.version1.8/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.19.1/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.8/version/dependency!--swagger--dependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger2/artifactIdversion2.9.2/version/dependency!--swagger-ui--dependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger-ui/artifactIdversion2.9.2/version/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-lang3/artifactIdversion3.4/versionscopecompile/scope/dependencydependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.11/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactIdoptionaltrue/optional/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdconfigurationexcludesexcludegroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactId/exclude/excludes/configuration/plugin/plugins/build/project
3、yml server.port9090spring.application.nameredlockspring.swagger2.enabledtruespring.redis.database0spring.redis.passwordspring.redis.timeout3000spring.redis.modesinglespring.redis.pool.conn-timeout3000spring.redis.pool.so-timeout3000spring.redis.pool.size10spring.redis.single.address1192.168.111.185:6381 spring.redis.single.address2192.168.111.185:6382 spring.redis.single.address3192.168.111.185:6383 4、配置文件
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;Configuration
EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {AutowiredRedisProperties redisProperties;BeanRedissonClient redissonClient1() {Config config new Config();String node redisProperties.getSingle().getAddress1();node node.startsWith(redis://) ? node : redis:// node;SingleServerConfig serverConfig config.useSingleServer().setAddress(node).setTimeout(redisProperties.getPool().getConnTimeout()).setConnectionPoolSize(redisProperties.getPool().getSize()).setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());if (StringUtils.isNotBlank(redisProperties.getPassword())) {serverConfig.setPassword(redisProperties.getPassword());}return Redisson.create(config);}BeanRedissonClient redissonClient2() {Config config new Config();String node redisProperties.getSingle().getAddress2();node node.startsWith(redis://) ? node : redis:// node;SingleServerConfig serverConfig config.useSingleServer().setAddress(node).setTimeout(redisProperties.getPool().getConnTimeout()).setConnectionPoolSize(redisProperties.getPool().getSize()).setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());if (StringUtils.isNotBlank(redisProperties.getPassword())) {serverConfig.setPassword(redisProperties.getPassword());}return Redisson.create(config);}BeanRedissonClient redissonClient3() {Config config new Config();String node redisProperties.getSingle().getAddress3();node node.startsWith(redis://) ? node : redis:// node;SingleServerConfig serverConfig config.useSingleServer().setAddress(node).setTimeout(redisProperties.getPool().getConnTimeout()).setConnectionPoolSize(redisProperties.getPool().getSize()).setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());if (StringUtils.isNotBlank(redisProperties.getPassword())) {serverConfig.setPassword(redisProperties.getPassword());}return Redisson.create(config);}}import lombok.Data;Data
public class RedisPoolProperties {private int maxIdle;private int minIdle;private int maxActive;private int maxWait;private int connTimeout;private int soTimeout;// 池大小private int size;
}
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;ConfigurationProperties(prefix spring.redis, ignoreUnknownFields false)
Data
public class RedisProperties {private int database;/*** 等待节点回复命令的时间。该时间从命令发送成功时开始计时*/private int timeout;private String password;private String mode;/*** 池配置*/private RedisPoolProperties pool;/*** 单机信息配置*/private RedisSingleProperties single;
}
import lombok.Data;Data
public class RedisSingleProperties {private String address1;private String address2;private String address3;
}
5、controller
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.RedissonMultiLock;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;RestController
Slf4j
public class RedLockController {public static final String CACHE_KEY_REDLOCK ATGUIGU_REDLOCK;AutowiredRedissonClient redissonClient1;AutowiredRedissonClient redissonClient2;AutowiredRedissonClient redissonClient3;boolean isLockBoolean;GetMapping(value /multiLock)public String getMultiLock() throws InterruptedException {String uuid IdUtil.simpleUUID();String uuidValue uuid:Thread.currentThread().getId();RLock lock1 redissonClient1.getLock(CACHE_KEY_REDLOCK);RLock lock2 redissonClient2.getLock(CACHE_KEY_REDLOCK);RLock lock3 redissonClient3.getLock(CACHE_KEY_REDLOCK);RedissonMultiLock redLock new RedissonMultiLock(lock1, lock2, lock3);redLock.lock();try {System.out.println(uuidValue\t---come in biz multiLock);try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(uuidValue\t---task is over multiLock);} catch (Exception e) {e.printStackTrace();log.error(multiLock exception ,e);} finally {redLock.unlock();log.info(释放分布式锁成功key:{}, CACHE_KEY_REDLOCK);}return multiLock task is over uuidValue;}}测试
测试地址http://localhost:9090/multilock
测试命令 ttl ATGUIGU REDLOCK HGETALL ATGUIGU_REDLOCK shutdown docker start redis-master-1 docker exec -it redis-master-1 redis-cli