(三)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 ...
随机推荐
- Java数组 —— 八大排序
(请观看本人博文--<详解 普通数组 -- Arrays类 与 浅克隆>) 在本人<数据结构与算法>专栏的讲解中,本人讲解了如何去实现数组的八大排序. 但是,在讲解的过程中,我 ...
- PHP反序列化漏洞总结
写在前边 做了不少PHP反序列化的题了,是时候把坑给填上了.参考了一些大佬们的博客,自己再做一下总结 1.面向对象 2.PHP序列化和反序列化 3.PHP反序列化漏洞实例 1.面向对象 在了解序列化和 ...
- Java中接口的概念
接口的特点: A:接口用关键字interface表示 interface 接口名 {} B:类实现接口用 implements 表示 class 类名 implements 接口名 {} C:接口不能 ...
- ES[7.6.x]学习笔记(三)新建索引
与ES的交互方式 与es的交互方式采用http的请求方式,请求的格式如下: curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT& ...
- 关于DNS解析:侧面剖析
作为一个合格的重度windows使用用户,我清楚的知道一个文件——hosts文件:C:\Windows\System32\drivers\etc\hosts文件 该文件需要一定的管理员权限. 这个文件 ...
- jquery选择时分插件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- php原生函数应用
php常见基本的函数 一.字符串函数 implode — 将一个一维数组的值转化为字符串 lcfirst — 使一个字符串的第一个字符小写 ltrim — 删除字符串开头的空白字符(或其他字符) rt ...
- 关于暴力破解的一些学习笔记(pikachu)
这几天的笔记都懒得发博客都写在本地了,随缘搬上来 什么是暴力破解 就是在攻击者不知道目标账号密码情况下的,对目标系统的常识性登陆 一般会采用一些工具+特定的字典 来实现高效的连续的尝试性登陆 一个有效 ...
- MySQL 入门(1):查询和更新的内部实现
摘要 在MySQL中,简单的CURD是很容易上手的. 但是,理解CURD的背后发生了什么,却是一件特别困难的事情. 在这一篇的内容中,我将简单介绍一下MySQL的架构是什么样的,分别有什么样的功能.然 ...
- QT使用提升自定义组件
QT使用提升自定义组件 QTC++QT自定义 QT 组件提升来实现自定义功能 介绍 我们在使用QT设置界面之后,往往需要自己实现一些方法,如果是单独 的还好,但是如果遇到很多同类型的都有需求, 比如 ...