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 &分布式锁的更多相关文章

  1. Redis分布式锁

    Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...

  2. redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  3. Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  4. Redis分布式锁---完美实现

    这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...

  5. 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

  6. Lua脚本在redis分布式锁场景的运用

    目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...

  7. Redlock(redis分布式锁)原理分析

    Redlock:全名叫做 Redis Distributed Lock;即使用redis实现的分布式锁: 使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击) ...

  8. 【分布式缓存系列】集群环境下Redis分布式锁的正确姿势

    一.前言 在上一篇文章中,已经介绍了基于Redis实现分布式锁的正确姿势,但是上篇文章存在一定的缺陷——它加锁只作用在一个Redis节点上,如果通过sentinel保证高可用,如果master节点由于 ...

  9. Redlock:Redis分布式锁最牛逼的实现

    普通实现 说道Redis分布式锁大部分人都会想到:setnx+lua,或者知道set key value px milliseconds nx.后一种方式的核心实现命令如下: - 获取锁(unique ...

随机推荐

  1. 数据结构和算法(Golang实现)(20)排序算法-选择排序

    选择排序 选择排序,一般我们指的是简单选择排序,也可以叫直接选择排序,它不像冒泡排序一样相邻地交换元素,而是通过选择最小的元素,每轮迭代只需交换一次.虽然交换次数比冒泡少很多,但效率和冒泡排序一样的糟 ...

  2. 第十节:xml、re、logging模块

    XML模块:(用到的时候再看)tree=xml.parse('xmltest.xml')root= tree.getroot()print(root.tag) 打印对象的标签root.attrib 获 ...

  3. Websocket直播间聊天室教程 - GoEasy快速实现聊天室

    最近两年直播那个火啊,真的是无法形容!经常有朋友问起,我想实现一个直播间聊天或者我想开发一个聊天室, 要如何开始呢? 今天小编就手把手的教你用GoEasy做一个聊天室,当然也可以用于直播间内的互动.全 ...

  4. Vmware下安装Linux

    Linux系统 开源的操作系统.主要是应用在软件的服务器,性能比windows要好. Linux系统(ubuntu,centos,redhat,aix....) Linux主要是通过命令去操作计算机, ...

  5. D - Three Integers CodeForces - 1311D

    题意: a<=b<=c 输出A,B,C要求B是A的倍数,C是B的倍数,并且输出a,b,c变成A,B,C需要的最小次数. 题解:写了半天的二分,后来发现思路错了,,,暴力就能过.. 三层fo ...

  6. 胜利大逃亡 BFS

    Ignatius被魔王抓走了,有一天魔王出差去了,这可是Ignatius逃亡的好机会. 魔王住在一个城堡里,城堡是一个A*B*C的立方体,可以被表示成A个B*C的矩阵,刚开始Ignatius被关在(0 ...

  7. G - GCD and LCM 杭电

    Given two positive integers G and L, could you tell me how many solutions of (x, y, z) there are, sa ...

  8. OkHttp 优雅封装 HttpUtils 之 上传下载解密

    曾经在代码里放荡不羁,如今在博文中日夜兼行,只为今天与你分享成果.如果觉得本文有用,记得关注我,我将带给你更多. 还没看过第一篇文章的欢迎移步:OkHttp 优雅封装 HttpUtils 之气海雪山初 ...

  9. 一站式轻量级框架 Spring

    Spring 简介 Spring 是一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的.Spring 的核心是控制反转(IoC)和面向切面编程(AOP).简单来说,Spring ...

  10. Mysql数据操作指令

    -----多数据插入-----只要写一次insert指令,但是可以直接插入多条记录insert into table values(),(),(); 主键冲突我们插入值的时候,主键中已经存在某个值,插 ...