(三)Redis &分布式锁
1 Redis使用中的常见问题和解决办法
1.1 缓存穿透
定义:缓存系统都是按照key去缓存查询,如果不存在对应的value,就应该去DB查找。一些恶意的请求会故意查询不存在的key,请求量很大,就会对DB造成很大的压力,甚至压垮数据库。
解决方案:对查询结果为空的情况也进行缓存,TTL设置短一点。
1.2 缓存雪崩
定义:在某个时间点,缓存中的key集体发生过期失效致使大量查询数据库的请求都落在DB上,导致DB负载过高、压力暴增,甚至可能压垮数据库
解决方案:该问题产生的原因在于大量的key在某个时间点或者某个时间段失效导致的,为了更好的避免这种问题的发生,一般更好的做法是为这些key设置不同的、随机的TTL(过期失效时间),从而错开缓存中的key的失效时间点。
1.3 缓存击穿
定义:缓存中某个频繁被访问的key(也称为热点key),在不停的扛着前端的高并发请求,当这个key突然在某个瞬间过期失效时,持续的高并发请求就击穿缓存,直接请求数据库,导致数据库压力在某一瞬间暴增。
解决方案:出现这个问题的原因在于热点的key过期失效了,而在实际情况中,既然这个key可以被作为热点频繁访问,那么就应该设置这个Key永不过期,这样前端的高并发请求将几乎永远不会落在数据库上。
2 高并发问题&Redis解决方案
在传统的单体Java应用中,为了解决多线程的并发安全问题,最常见的做法是在核心的业务逻辑代码中加锁操作进行同步控制如synchronized关键字,然而在微服务、分布式系统架构时代,这种做法是行不通的,因为synchronized关键字是跟单一服务节点所在的JVM相关联的,而分布式系统架构下的服务一般是部署在不同的节点(服务器)下,从而当出现高并发请求时,Synchronized同步操作将显得力不从心。因而我们需要寻找一种高效的解决方案,这种方案既要保证单一节点核心业务代码的同步控制,也要保证当扩展到多个节点部署时同样能实现核心逻辑代码的同步控制,由此分布式锁应运而生。它的出现主要是为了解决分布式系统中高并发请求时并发访问共享资源导致并发安全问题,目前关于分布式锁的实现有许多种,典型的包括基于数据库级别的乐观锁和悲观锁,以及关于Redis的原子操作实现分布式锁和基于ZooKeeper实现分布式锁等。
在Redis的知识体系和底层基础架构中,其实并没有直接提供所谓的分布式锁组件,而是间接的借助其原子操作来实现。之所以原子操作可以实现分布式锁的功能,主要是得益于Redis的单线程机制,即不管外层应用系统并发了N个线程,当每个线程都需要Redis的某个原子操作时,是需要进行排队等待的,原因在于其底层系统架构中,同一时刻、同一个部署节点中只有一个线程执行某种原子操作。
3 Redis缺陷&Redisson解决方案
3.1 分析
Redis的原子操作实现的分布式锁具有一定的缺陷,包括:
(1)执行Redis的原子操作EXPIRE时,需要设置Key的过期时间TTL,不同的业务场景设置的过期时间是不同的,但是如果设置不当,将很有可能影响系统和Redis服务的性能。
(2)采用Redis的原子操作SETNX获取分布式锁时,不具备可重入性,即当高并发产生多线程时,同一时刻只有一个线程可以获取到锁,从而操作共享资源,而其他的线程将获取锁失败,而且是永远失败下去,而有一些业务需要要求线程"可重入",则需要在应用程序里添加while(true){}的代码块,即不断的循环等待获取分布式锁,这种方式既不优雅,又很可能造成应用系统性能卡顿。典型的场景如商城有些秒杀活动中,商家为了饥饿营销会故意将库存量设置为很小,但是没货时又及时补货,因此这种情况下就要求用户秒杀过程中的分布式锁的获取具有重入性。但显然Redis不适合这类场景。
(3)在执行Redis的原子操作SETNX之后EXPIRE操作之前,如果此时Redis的服务节点发生宕机,由于Key没有及时被释放而导致最终很有可能出现死锁,即永远不会有其他的线程能够获取到锁。
Redisson分布式锁的出现能够很好的解决以上问题,Redisson提供了多种分布式锁供开发者使用,包括可重入锁、一次性锁、联锁、读写锁等等,每一种分布式锁实现方式和使用的场景各不相同,而应用较多的当属Redission的可重入锁和一次性锁。可重入锁指的是当前线程如果没有获取到对共享资源的锁,将会在允许的时间范围内等待获取,超时则放弃等待,主要通过调用tryLock()方法时进行指定。一次性锁指的是当前线程获取分布式锁时,如果成功则执行后续对共享资源的操作,否则将永远失败下去,其主要通过调用lock()方法获取锁。
3.2 一次性锁与可重入锁的实现
(1)依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.</version>
</dependency>
(2)application.properties配置:
redisson.host.config=redis://127.0.0.1:6379
(3)客户端实例注入:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment; /**
* 自定义注入配置操作Redisson的客户端实例
* @author huaiheng
**/
@Configuration
public class RedissonConfig { @Autowired
private Environment environment; @Bean
public RedissonClient config(){
Config config = new Config();
config.useSingleServer().setAddress(environment.getProperty("redisson.host.config")).setKeepAlive(true);
return Redisson.create(config);
} }
(4)一次性锁 & 可重入锁
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired; import java.util.concurrent.TimeUnit; /**
* 可重入锁与一次性锁框架
*
* @author huaiheng
*/
public class RedissonLock { @Autowired
private RedissonClient redissonClient; /**
* 针对用户级别的一次性锁
*/
public void lockOnlyOnce(String UserId) throws Exception {
final String lockName = "redissionOneLock-" + UserId;
RLock rLock = redissonClient.getLock(lockName);
try {
// 上锁后,不管何种情况,10s后主动释放
rLock.lock(, TimeUnit.SECONDS);
/**
资源操作代码部分
**/
} catch (Exception e) {
throw e;
} finally {
if (rLock != null) {
rLock.unlock();
}
}
} /**
* 针对用户级别的可重入锁
*/
public void repeatLock(String UserId) throws Exception {
final String lockName = "redissionOneLock-" + UserId;
RLock rLock = redissonClient.getLock(lockName);
try {
// 尝试获取锁,时间最长为100s,如果获取到锁,则不管何种情况,最长10s必须释放
rLock.tryLock(, , TimeUnit.SECONDS);
/**
资源操作代码部分
**/
} catch (Exception e) {
throw e;
} finally {
if (rLock != null) {
rLock.unlock();
}
}
}
}
4 其他解决方案
除了Redis和Redisson提供的分布式锁的功能外,最常见的还包括数据库级别的分布锁机制,数据库的分布式锁主要包括乐观锁和悲观锁,在这里仅做原理和应用上的分析和讲解:
- 乐观锁:乐观锁是一种很佛系的实现方式,它总是认为不会产生并发问题,因为每次从数据库中获取数据时总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新数据时其会判断其他线程在之前是否对数据进行了修改(通常采用版本号version机制进行实现),如果没有进行修改则本次修改生效并更新版本号,如果已经被修改则本次修改无效,从而避免了多并发线程访问共享数据时出现数据不一致的现象,这种实现方法对应的核心SQL的伪代码写法如下所示:
update table set key=value, version=version+ where id=#{id} and version=#{version};
- 悲观锁:悲观锁是一种消极的处理方式,它总是假设事情的发生在最坏的情况,即每次并发线程在获取数据的时候认为其他线程对数据进行了修改,因而每次在获取数据时都会上锁,而其他线程访问该数据时就会发生阻塞的现象,最终只有当前线程释放了该共享资源的锁,其他线程才能获取到锁,并对共享资源进行操作。数据库中的行锁、表锁、读锁、写锁都是这种方式,java中的synchronized和ReentrantLock也是悲观锁的思想。
应用场景分析:
- 乐观锁:由于采用version版本号机制实现,因而在高并发产生多线程时,同一时刻只有一个线程能够获取到锁并成功操作共享资源,而其他线程将获取失败并且是永远失败下去,从这个角度来看,这种方式虽然可以控制并发线程对共享资源的访问,但是却牺牲了系统的吞吐性能,因此适用于读多写少的场景。
- 悲观锁:由于建立在数据库底层搜索引擎的基础之上,并采用select... for update方式对共享资源加锁,因而当产生高并发多线程请求,特别是读请求时,将对数据库的性能带来更严重的影响,特别是同一时刻产生的多线程中将只有一个线程能够获取到锁,而其他线程将处于阻塞的状态,直到该线程释放了锁,从这一角度来看,基于数据级别的悲观锁适用于并发量不大的情况,特别是读请求数据量不大的情况,因此适用于读少写多的场景。
(三)Redis &分布式锁的更多相关文章
- Redis分布式锁
Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...
- redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁---完美实现
这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...
- 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...
- Lua脚本在redis分布式锁场景的运用
目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...
- Redlock(redis分布式锁)原理分析
Redlock:全名叫做 Redis Distributed Lock;即使用redis实现的分布式锁: 使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击) ...
- 【分布式缓存系列】集群环境下Redis分布式锁的正确姿势
一.前言 在上一篇文章中,已经介绍了基于Redis实现分布式锁的正确姿势,但是上篇文章存在一定的缺陷——它加锁只作用在一个Redis节点上,如果通过sentinel保证高可用,如果master节点由于 ...
- Redlock:Redis分布式锁最牛逼的实现
普通实现 说道Redis分布式锁大部分人都会想到:setnx+lua,或者知道set key value px milliseconds nx.后一种方式的核心实现命令如下: - 获取锁(unique ...
随机推荐
- Ant安装与配置
1. 到apache 官网去下载最新版本的ant,http://ant.apache.org/:下载后直接解压缩到电脑上,不需要安装: 2.环境变量配置: 2.1 ->计算机右键->属性- ...
- 用threejs实现三维全景图
网络上看到了3D全景图,发现threejs里面有一个库竟然可以实现,一下我贴出代码: <!DOCTYPE html> <html> <head> <meta ...
- 化繁为简,弱监督目标定位领域的新SOTA - 伪监督目标定位方法(PSOL) | CVPR 2020
论文提出伪监督目标定位方法(PSOL)来解决目前弱监督目标定位方法的问题,该方法将定位与分类分开成两个独立的网络,然后在训练集上使用Deep descriptor transformation(DDT ...
- Delphi学习手记——单引号和双引号的区别
单引号和双引号的区别 双引号表示其中字符可能包含变量,而单引号表示整个引号内的东西都当成字符串来处理. 也就是说:没有内设变量就用单引号'',有就用双引号"". 举例说明: $va ...
- Spring Data REST不完全指南(二)
上一篇文章介绍了Spring Data REST的功能及特征,以及演示了如何在项目中引入Spring Data REST并简单地启动演示了Spring Data REST项目.在本文中,我们将深入了解 ...
- python机器学习入门-(1)
机器学习入门项目 如果你和我一样是一个机器学习小白,这里我将会带你进行一个简单项目带你入门机器学习.开始吧! 1.项目介绍 这个项目是针对鸢尾花进行分类,数据集是含鸢尾花的三个亚属的分类信息,通过机器 ...
- Linux 平台 安装 Composer
1.检查是否安装 composer --version 2.下载安装 php -r "copy('https://install.phpcomposer.com/installer', 'c ...
- 【题解】P3959 宝藏 - 状压dp / dfs剪枝
P3959 宝藏 题目描述 参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的m 条道路和它们的长度. 小明决心亲自前往挖掘所有宝 ...
- liunx常用知识基本命令大全
liunx基础命令使用 标签(空格分隔):liunx常用命令 网络配置 虚拟网卡的绝对路径 /etc/sysconfig/network-scripts/ifcfg-eth0 DEVICE=eth0 ...
- 2019-2020-1 20199310《Linux内核原理与分析》第五周作业
1.问题描述 在前面的文章中,已经了解了Linux内核源代码的目录结构,并在Oracle VM VirtualBox的Linux环境中构造一个简单的操作系统MenuOS,本文将学习系统调用的相关理论知 ...