热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

详解Redis分布式锁(图文并茂,手把手搭建服务,代码详解,建议收藏)

本文章涉及很多关联篇,如需要请自取(关注博主,我们一起去大厂happy)VMware安装CentOS(保姆级教

本文章涉及很多关联篇,如需要请自取(关注博主,我们一起去大厂happy)

VMware安装CentOS(保姆级教程,建议收藏)

VMware+CentOS 7 静态IP设置方法 —保姆级教程,建议收藏

CentOS 7关闭防火墙

CentOS安装单击Redis

Redis一主二从Sentinel监控配置(保姆级教程)


一、简介

分布式知识是考验一个程序员知识面广度和深度很好的度量标准,而分布式锁又是其中非常重要的一个知识点。可以说面试只要谈到分布式,没有不问分布式锁的。
我们知道分布式应用在进行逻辑处理的时候经常涉及到并发的问题,比如对一个转账修改一个用户的账户金额数据,此时可能会涉及多个用户同时对这个用户进行转账,它们都需要将数据读取到各内存中进行修改,然后再存刷新回去,这个时候就会出现并发问题:
这里假设初始account.money = 400

  1. 客户端A,向账户转账100,并未提交到Redis
  2. 客户端B,读取金额为400,并向账户转账200
  3. 客户端A提交
  4. 客户端B提交

最后account.money = 600
执行流程如下所示:

并发场景示例.png


出现上述问题的主要原因是,“读取”和“写入”这两个操作并不是一个原子操作(所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程))。
关于上述问题的解决,分布式锁就可以派上用场了,我们通过分布式锁来限制程序的并发执行,就像Java中的synchronized,常见的分布式锁解决方案如下所示:

  1. 基于数据库锁机制实现的分布式锁
  2. 基于Zookeeper实现的分布式锁
  3. 基于Redis实现的分布式锁

本文和大家共同探讨的是基于Redis的分布式锁。

二、分布式锁的演进


2.1 精细胞与卵细胞的爱情故事

嗯……学过生物的宝宝们都知道,精细胞和卵细胞相遇的故事,上亿精细胞中最后也只会有一个幸运的精细胞和卵细胞发生甜蜜的爱情故事,这是因为当有一个精子进入卵子后,卵子会发生皮质反应、透明带反应使透明带对精子的结合能力下降,阻止了多精受精。分布式锁就好像卵细胞,而万千访问客户端就好比精细胞,无论客户访问并发多激烈,我们也应该保证分布式锁只被一个线程获取到。

u=131622752,2670653155&fm=26&fmt=auto&gp=0.webp

2.2.2 setnx + expire

在第一种解决方案的基础上,可能部分人会相到,既然主动删除key可能会出现异常情况,那么就设值key的过期时间到期自动删除。

127.0.0.1:6379> setnx lock true
(integer) 1
127.0.0.1:6379> expire lock 10 # 设值过期时间10s
(integer) 1
127.0.0.1:6379> setnx lock true # 10s内再次设值失败
(integer) 0
127.0.0.1:6379> setnx lock true # 10skey过期,后设置成功
(integer) 1

这种的方案和前面的方案其实并没有本质上的区别,它还是可能会出现服务器异常等情况,导致expire的不到执行的情况,换汤不换药,如下图所示:

setnx + expire.png

2.2.3 原子操作

基于上面两种方案,我们可以发现,产生问题的本质在于两个操作并不是原子操作。方案一中是setnx指令加一个del指令,方案二中是setnx指令加一个expire指令,这两个指令并不是原子指令。基于这个问题,Redis官方将这两个指令组合在了一起,解决Redis分布式锁原子性操作的问题。
认真看set指令可选参数 EX 和 NX

set key value [EX seconds] [PX milliseconds] [NX|XX]




EX seconds :将键的过期时间设置为 seconds 秒。执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
PX milliseconds :将键的过期时间设置为 milliseconds 毫秒。执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
NX:只在键不存在时,才对键进行设置操作。执行 SET key value NX 的效果等同于执行 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。

127.0.0.1:6379> set lock true EX 10 NX        # 设置 10s生效
OK
127.0.0.1:6379> set lock true EX 10 NX        # 10s内再次设值失败
(nil)
127.0.0.1:6379> set lock true EX 10 NX        # 10s后设置成功
OK

如上这个操作就成功的解决了Redis分布式锁的原子操作问题。

2.2.4 解锁

Redis分布式锁加锁在上面讲述了,而Redis分布式锁的解锁过程其实就是将key删除,key的删除有客户端调用del指令删除,也有设置key的过期时间自动删除。但是这个删除不能乱删除,不能说客户端A请求的锁被客户端B给删除了……,那这把锁就是一把烂锁了。
为了防止客户端A请求的锁被客户端B给删除了这种情况,我们通过匹配客户端传入的锁的值与当前锁的值是否相等来做判断(这个值是随机且保证不会重复的),如果相等就删除,解锁成功。
但是Redis并未提供这样的功能,我们只能通过Lua脚本来处理,因为Lua脚本可以保证多个指令的原子性执行。
 

示例:
首先设置一个key,这个key的值是123456789,通过客户端传入的value值是否相等来校验是否允许删除这个key

127.0.0.1:6379> get lock
(nil)
127.0.0.1:6379> set lock 123456789 # 设置一个key 值为123456789
OK
127.0.0.1:6379> get lock
"123456789"

在客户机上编写lua脚本,lock.lua文件,文件内容如下


image.png

if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])
elsereturn 0
end

测试通过错误的value值去执行lua脚本,这个时候删除key失败,返回0

image.png

通过正确的value值执行则返回1,说明key被删除了。
image.png

2.2.5 代码实现

一下演示一个spring boot项目来实现Redis分布式锁,为了方便大家使用,我贴出的代码比较全面,篇幅稍多。
pom依赖

org.springframework.bootspring-boot-starter-parent2.3.4.RELEASE
org.springframework.bootspring-boot-starter-webredis.clientsjedis3.0.1org.projectlomboklombokcn.hutoolhutool-all5.3.4

Redis配置文件

package com.lizba.config;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;/***

* Redis简单配置文件*

** @Author: Liziba* @Date: 2021/7/11 11:17*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport {protected static final Logger logger = LoggerFactory.getLogger(RedisConfig.class);@Value("${spring.redis.host}")private String host;@Value("${spring.redis.port}")private int port;@Value("${spring.redis.jedis.pool.max-active}")private int maxTotal;@Value("${spring.redis.jedis.pool.max-idle}")private int maxIdle;@Value("${spring.redis.jedis.pool.min-idle}")private int minIdle;@Value("${spring.redis.password}")private String password;@Value("${spring.redis.timeout}")private int timeout;@Beanpublic JedisPool redisPoolFactory() {JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();jedisPoolConfig.setMaxTotal(maxTotal);jedisPoolConfig.setMaxIdle(maxIdle);jedisPoolConfig.setMinIdle(minIdle);JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);logger.info("JedisPool注入成功!!");logger.info("redis地址:" + host + ":" + port);return jedisPool;}
}

application.yml配置文件

server:port: 18080spring:redis:database: 0host: 127.0.0.1port: 6379timeout: 10000password:jedis:pool:max-active: 20max-idle: 20min-idle: 0

获取锁与释放锁代码

package com.lizba.utill;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;import java.util.Arrays;
import java.util.concurrent.TimeUnit;/***

* Redis分布式锁简单工具类*

** @Author: Liziba* @Date: 2021/7/11 11:42*/
@Service
public class RedisLockUtil {private static Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);/*** 锁键 -> key*/private final String LOCK_KEY = "lock_key";/*** 锁过期时间 -> TTL*/private Long millisecondsToExpire = 10000L;/*** 获取锁超时时间 -> get lock timeout for return*/private Long timeout = 300L;/*** LUA脚本 -> 分布式锁解锁原子操作脚本*/private static final String LUA_SCRIPT ="if redis.call('get',KEYS[1]) == ARGV[1] then" +" return redis.call('del',KEYS[1]) " +"else" +" return 0 " +"end";/*** set命令参数*/private SetParams params = SetParams.setParams().nx().px(millisecondsToExpire);@Autowiredprivate JedisPool jedisPool;/*** 加锁 -> 超时锁** @param lockId 一个随机的不重复id -> 区分不同客户端* @return*/public boolean timeLock(String lockId) {Jedis client = jedisPool.getResource();long start = System.currentTimeMillis();try {for(;;) {String lock = client.set(LOCK_KEY, lockId, params);if ("OK".equalsIgnoreCase(lock)) {return Boolean.TRUE;}// sleep -> 获取失败暂时让出CPU资源TimeUnit.MILLISECONDS.sleep(100);long time = System.currentTimeMillis() - start;if (time >= timeout) {return Boolean.FALSE;}}} catch (Exception e) {e.printStackTrace();logger.error(e.getMessage());} finally {client.close();}return Boolean.FALSE;}/*** 解锁** @param lockId 一个随机的不重复id -> 区分不同客户端* @return*/public boolean unlock(String lockId) {Jedis client = jedisPool.getResource();try {Object result = client.eval(LUA_SCRIPT, Arrays.asList(LOCK_KEY), Arrays.asList(lockId));if (result != null && "1".equalsIgnoreCase(result.toString())) {return Boolean.TRUE;}return Boolean.FALSE;} catch (Exception e) {e.printStackTrace();logger.error(e.getMessage());}return Boolean.FALSE;}}

测试类

package com.lizba.controller;import cn.hutool.core.util.IdUtil;
import com.lizba.utill.RedisLockUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;/***

* 测试*

** @Author: Liziba* @Date: 2021/7/11 12:27*/
@RestController
@RequestMapping("/redis")
public class TestController {&#64;Autowiredprivate RedisLockUtil redisLockUtil;private AtomicInteger count ;&#64;GetMapping("/index/{num}")public String index(&#64;PathVariable int num) throws InterruptedException {count &#61; new AtomicInteger(0);CountDownLatch countDownLatch &#61; new CountDownLatch(num);ExecutorService executorService &#61; Executors.newFixedThreadPool(num);Set failSet &#61; new HashSet<>();long start &#61; System.currentTimeMillis();for (int i &#61; 0; i {long lockId &#61; IdUtil.getSnowflake(1, 1).nextId();try {boolean isSuccess &#61; redisLockUtil.timeLock(String.valueOf(lockId));if (isSuccess) {count.addAndGet(1);System.out.println(Thread.currentThread().getName() &#43; " lock success" );} else {failSet.add(Thread.currentThread().getName());}} finally {boolean unlock &#61; redisLockUtil.unlock(String.valueOf(lockId));if (unlock) {System.out.println(Thread.currentThread().getName() &#43; " unlock success" );}}countDownLatch.countDown();});}countDownLatch.await();executorService.shutdownNow();failSet.forEach(t -> System.out.println(t &#43; " lock fail" ));long time &#61; System.currentTimeMillis() - start;return String.format("Thread sum: %d, Time sum: %d, Success sum&#xff1a;%d", num, time, count.get());}}

测试结果

image.png


image.png


2.3 Redis的超时问题

Redis分布式锁有一个问题是锁的超时问题&#xff0c;也就是说如果客户端A获取到锁之后去执行任务&#xff0c;任务没跑完锁的超时时间到了&#xff0c;锁就会自动释放&#xff0c;这个时候客户端B就能乘虚而入了&#xff0c;锁就会出现问题&#xff01;
关于这个问题其实并没有完全的解决办法&#xff0c;但是能通过如下手段去优化&#xff1a;

  1. 尽可能不要在Redis分布式锁中执行较长的任务&#xff0c;尽可能的缩小锁区间内执行代码&#xff0c;就像单JVM锁中的synchronized优化一样&#xff0c;我们可以考虑优化锁的区间
  2. 多做压力测试和线上真实场景的模拟测试&#xff0c;估算一个合适的锁超时时间
  3. 做好Redis分布式锁超时任务未执行完的问题发生后&#xff0c;数据恢复手段的准备


三、集群中的分布式锁


3.1 集群分布式锁存在的问题

上述的分布式锁&#xff0c;针对单节点实例的Redis是可行的&#xff1b;但是我们在公司根本不会用单节点的Redis实例&#xff0c;往往采用最简单的都是是Redis一主二从&#43;Sentinel监控配置&#xff1b;在sentinel集群中&#xff0c;虽然主节点挂掉时&#xff0c;从节点会取而代之&#xff0c;客户端无感知&#xff0c;但是上述的分布式锁就可能存在节点之间数据同步异常导致分布式锁失效的问题。
正常情况下客户端向sentinel监控的Redis集群申请分布式锁&#xff1a;

Redis哨兵模式申请锁.png

比如&#xff0c;客户端A在主节点&#xff08;机器1&#xff09;上申请了一把锁&#xff0c;此时主节点&#xff08;机器1&#xff09;挂掉了且锁没来得及同步到从节点&#xff08;机器2和机器3&#xff09;&#xff0c;此时从节点&#xff08;机器2&#xff09;成为了新的主节点&#xff0c;但是锁在新的主节点&#xff08;机器2&#xff09;上并不存在&#xff0c;所以客户端B申请锁成功&#xff0c;锁的定义在这种场景中就出现了问题&#xff01;
主节点宕机锁同步失败情况&#xff0c;其他客户端申请锁成功&#xff1a;
Redis哨兵模式master宕机申请锁.png

上述这种情况虽然之后发生在主从发生failover的情况才产生&#xff0c;但显然是不安全的&#xff0c;普通的业务系统或许能接受&#xff0c;但大金额的业务场景是不允许出现的。


3.2 RedLock

3.2.1 简介

解决这个问题的办法就是使用RedLock算法&#xff0c;也称红锁。RedLock通过使用多个Redis实例&#xff0c;各个实例之间没有主从关系&#xff0c;相互独立&#xff1b;加锁的时候&#xff0c;客户端向所有的节点发送加锁指令&#xff0c;如果过半的节点set成功&#xff0c;就加锁成功。释放锁时&#xff0c;需要向所有的节点发送del指令来释放锁。RedLock的实现思路比较简单&#xff0c;但是实际算法比较复杂&#xff0c;需要考虑非常多的细节问题&#xff0c;如出错重试&#xff0c;时钟漂移等。此外RedLock需要新增较多的实例来申请分布式锁&#xff0c;不仅消耗服务器资源&#xff0c;也会有一定的性能下降。
其架构图如下&#xff0c;客户端向多个独立的Redis服务发送加锁指令&#xff08;为了追求高吞吐量和低延时&#xff0c;客户端需要使用多路传输来对N个Redis Server服务器进行通信&#xff09;&#xff0c;过半反馈成功则加锁成功&#xff0c;Redis Server的个数最后为奇数。
Redis 中文版网站介绍

REDIS distlock -- Redis中国用户组&#xff08;CRUG&#xff09;

在上述的架构图中&#xff0c;存在5台Redis服务器用于获取锁&#xff0c;那么此时一个客户端获取锁需要做哪些操作呢&#xff1f;

  1. 获取系统当前时间(ms)
  2. 使用相同的key和随机值在5个节点上请求锁&#xff0c;请求锁的过程中包含多个时间值的定义&#xff0c;包括请求单个锁超时时间&#xff0c;请求锁的总耗时时间&#xff0c;锁自动释放时间。单个锁请求的超时时间不宜过大&#xff0c;防止请求宕机的Redis服务阻塞时间过长。
  3. 客户端计算获取锁的总时长和获取锁成功的个数&#xff0c;当所得个数大于等于3且获取锁的时间小于锁的自动释放时间才算成功
  4. 锁获取成功&#xff0c;则锁自动释放时间等于TTL减去获取所得消耗的时间&#xff08;这个锁消耗的时间计算比较复杂&#xff09;
  5. 锁获取失败&#xff0c;向所有的Redis服务器发送删除指令&#xff0c;一定是所有的Redis服务器都要发送
  6. 失败重试&#xff0c;锁获取失败后进行重试&#xff0c;重试的时间应该是一个随机值&#xff0c;避免与其他客户端同时请求锁而加大失败的可能&#xff0c;且这个时间应该大于获取锁消耗的时间

3.2.2 锁的最小有效时长

由于上面说到存在时钟漂移的问题&#xff0c;并且客户端向不同的Redis服务器请求锁的时间也会有细微的差异&#xff0c;所以有必要认真的研究一下客户端获取到的锁的最小有效时长计算&#xff1a;
假设客户端申请锁成功&#xff0c;第一个key设置成功的时间为TF&#xff0c;最后一个key设置成功的时间为TL&#xff0c;锁的超时时间为TTL&#xff0c;不同进程之间的时钟差异为CLOCK_DIFF&#xff0c;则锁的最小有效时长是&#xff1a;

TIME &#61; TTL - (TF- TL) - CLOCK_DIFF

RedLock锁的有效时长.png

3.2.3 故障恢复

采用Redis来实现分布式锁&#xff0c;离不开服务器宕机等不可用问题&#xff0c;这里RedLock红锁也一样&#xff0c;即使是多台服务器申请锁&#xff0c;我们也要考虑服务器宕机后的处理&#xff0c;官方建议采用AOF持久化处理。
但是AOF持久化只对正常SHUTDOWN这种指令能做到重启恢复&#xff0c;但是如果是断电的情况&#xff0c;可能导致最后一次持久化到断电期间的锁数据丢失&#xff0c;当服务器重启后&#xff0c;可能会出现分布式锁语义错误的情况。所以为了规避这种情况&#xff0c;官方建议Redis服务重启后&#xff0c;一个最大客户端TTL时间内该Redis服务不可用&#xff08;不提供申请锁的服务&#xff09;&#xff0c;这确实可以解决问题&#xff0c;但是显而易见这肯定影响Redis服务器的性能&#xff0c;并且在多数节点都出现这种情况的时候&#xff0c;系统将出现全局不可用的状态。

3.3 Redisson实现分布式锁

3.3.1 Redission简介

Redisson是架设在Redis基础上的一个Java驻内存数据网格&#xff08;In-Memory Data Grid&#xff09;。【Redis官方推荐】
Redisson在基于NIO的Netty框架上&#xff0c;充分的利用了Redis键值数据库提供的一系列优势&#xff0c;在Java实用工具包中常用接口的基础上&#xff0c;为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力&#xff0c;大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务&#xff0c;更进一步简化了分布式环境中程序相互之间的协作。

Redission github地址&#xff1a;

https://github.com/redisson/redisson

Redission 分布式锁和同步器Wiki&#xff1a;

https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

总而言之——Redisson非常强大

3.3.2 Reddison之RedLock使用

pom依赖

org.redissonredisson3.3.2

测试类

package com.liziba.util;import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;import java.util.concurrent.TimeUnit;/***

* 测试Redisson 之 RedLock*

** &#64;Author: Liziba* &#64;Date: 2021/7/11 20:55*/
public class LockTest {private static final String resourceName &#61; "REDLOCK_KEY";private static RedissonClient cli_79;private static RedissonClient cli_89;private static RedissonClient cli_99;static {Config config_79 &#61; new Config();config_79.useSingleServer().setAddress("127.0.0.1:6379") // 注意这里我的Redis测试实例没密码.setDatabase(0);cli_79 &#61; Redisson.create(config_79);Config config_89 &#61; new Config();config_89.useSingleServer().setAddress("127.0.0.1:6389").setDatabase(0);cli_89 &#61; Redisson.create(config_89);Config config_99 &#61; new Config();config_99.useSingleServer().setAddress("127.0.0.1:6399").setDatabase(0);cli_99 &#61; Redisson.create(config_99);}/*** 加锁操作*/private static void lock () {// 向3个Redis实例尝试加锁RLock lock_79 &#61; cli_79.getLock(resourceName);RLock lock_89 &#61; cli_89.getLock(resourceName);RLock lock_99 &#61; cli_99.getLock(resourceName);RedissonRedLock redLock &#61; new RedissonRedLock(lock_79, lock_89, lock_99);try {boolean isLock &#61; redLock.tryLock(100, 10000, TimeUnit.MILLISECONDS);if (isLock) {// do something ...System.out.println(Thread.currentThread().getName() &#43; "Get Lock Success!");TimeUnit.MILLISECONDS.sleep(10000);} else {System.out.println(Thread.currentThread().getName() &#43; "Get Lock fail!");}} catch (InterruptedException e) {e.printStackTrace();} finally {// 无论如何一定要释放锁 -> 这里会像所有的Redis服务释放锁redLock.unlock();}}public static void main(String[] args) {for (int i &#61; 0; i <10; i&#43;&#43;) {new Thread(() -> lock()).start();}}}

3.3.3 ReddisonRedLock之源码分析

敬请关注….


推荐阅读
  • 提升Android开发效率:Clean Code的最佳实践与应用
    在Android开发中,提高代码质量和开发效率是至关重要的。本文介绍了如何通过Clean Code的最佳实践来优化Android应用的开发流程。以SQLite数据库操作为例,详细探讨了如何编写高效、可维护的SQL查询语句,并将其结果封装为Java对象。通过遵循这些最佳实践,开发者可以显著提升代码的可读性和可维护性,从而加快开发速度并减少错误。 ... [详细]
  • 分布式开源任务调度框架 TBSchedule 深度解析与应用实践
    本文深入解析了分布式开源任务调度框架 TBSchedule 的核心原理与应用场景,并通过实际案例详细介绍了其部署与使用方法。首先,从源码下载开始,详细阐述了 TBSchedule 的安装步骤和配置要点。接着,探讨了该框架在大规模分布式环境中的性能优化策略,以及如何通过灵活的任务调度机制提升系统效率。最后,结合具体实例,展示了 TBSchedule 在实际项目中的应用效果,为开发者提供了宝贵的实践经验。 ... [详细]
  • 2019年后蚂蚁集团与拼多多面试经验详述与深度剖析
    2019年后蚂蚁集团与拼多多面试经验详述与深度剖析 ... [详细]
  • 2020年9月15日,Oracle正式发布了最新的JDK 15版本。本次更新带来了许多新特性,包括隐藏类、EdDSA签名算法、模式匹配、记录类、封闭类和文本块等。 ... [详细]
  • 本文详细介绍了Java代码分层的基本概念和常见分层模式,特别是MVC模式。同时探讨了不同项目需求下的分层策略,帮助读者更好地理解和应用Java分层思想。 ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 在Linux系统中避免安装MySQL的简易指南
    在Linux系统中避免安装MySQL的简易指南 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • Kafka 是由 Apache 软件基金会开发的高性能分布式消息系统,支持高吞吐量的发布和订阅功能,主要使用 Scala 和 Java 编写。本文将深入解析 Kafka 的安装与配置过程,为程序员提供详尽的操作指南,涵盖从环境准备到集群搭建的每一个关键步骤。 ... [详细]
  • 第二章:Kafka基础入门与核心概念解析
    本章节主要介绍了Kafka的基本概念及其核心特性。Kafka是一种分布式消息发布和订阅系统,以其卓越的性能和高吞吐量而著称。最初,Kafka被设计用于LinkedIn的活动流和运营数据处理,旨在高效地管理和传输大规模的数据流。这些数据主要包括用户活动记录、系统日志和其他实时信息。通过深入解析Kafka的设计原理和应用场景,读者将能够更好地理解其在现代大数据架构中的重要地位。 ... [详细]
  • 本文详细介绍了如何安全地手动卸载Exchange Server 2003,以确保系统的稳定性和数据的完整性。根据微软官方支持文档(https://support.microsoft.com/kb833396/zh-cn),在进行卸载操作前,需要特别注意备份重要数据,并遵循一系列严格的步骤,以避免对现有网络环境造成不利影响。此外,文章还提供了详细的故障排除指南,帮助管理员在遇到问题时能够迅速解决,确保整个卸载过程顺利进行。 ... [详细]
  • 2016-2017学年《网络安全实战》第三次作业
    2016-2017学年《网络安全实战》第三次作业总结了教材中关于网络信息收集技术的内容。本章主要探讨了网络踩点、网络扫描和网络查点三个关键步骤。其中,网络踩点旨在通过公开渠道收集目标信息,为后续的安全测试奠定基础,而不涉及实际的入侵行为。 ... [详细]
  • 【并发编程】全面解析 Java 内存模型,一篇文章带你彻底掌握
    本文深入解析了 Java 内存模型(JMM),从基础概念到高级特性进行全面讲解,帮助读者彻底掌握 JMM 的核心原理和应用技巧。通过详细分析内存可见性、原子性和有序性等问题,结合实际代码示例,使开发者能够更好地理解和优化多线程并发程序。 ... [详细]
  • 如何正确配置与使用日志组件:Log4j、SLF4J及Logback的连接与整合方法
    在当前的软件开发实践中,无论是开源项目还是日常工作中,日志框架都是不可或缺的工具之一。本文详细探讨了如何正确配置与使用Log4j、SLF4J及Logback这三个流行的日志组件,并深入解析了它们之间的连接与整合方法,旨在帮助开发者高效地管理和优化日志记录流程。 ... [详细]
author-avatar
mobiledu2502929507
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有