• [问题求助] redis的key和value有什么设计规则吗?
    大家平时redis设置key和value的时候一般是什么样的规则?或者会遵守什么样的规则呢?
  • [技术干货] CHAR和VARCHAR的区别
    问题描述这是关系型数据库中字符串存储类型的经典面试题面试官通过这个问题考察对数据库底层存储原理的理解通常会追问使用场景和性能影响核心答案CHAR和VARCHAR的主要区别:存储方式CHAR:固定长度存储,不足部分用空格填充VARCHAR:可变长度存储,根据实际内容长度分配空间存储空间CHAR(n):总是占用n个字符的空间VARCHAR(n):只占用实际字符长度+1或2个字节的额外空间(用于记录长度)性能特点CHAR:读写性能较稳定,适合固定长度数据VARCHAR:空间利用率高,适合变长数据详细解析1. CHAR特点它的特点为固定长度(最大255字符)、存储时空格填充到指定长度、检索时默认删除尾部空格、适合存储长度变化很小的数据2. VARCHAR特点它的特点为可变长度(MySQL 5.0.3之后最多可达65,535字节)、存储时需要1-2个字节记录长度、检索时不删除尾部空格、长度小于255使用1字节存储长度信息,否则使用2字节。需要注意的是在频繁更新的列上可能导致碎片化常见追问Q1: 什么场景下选择CHAR?A:存储长度几乎相等的字符串(如:邮政编码、手机号码)经常更新的字段(避免碎片)短字符串且长度固定(如:Y/N标识)频繁访问的表(减少碎片,提高性能)Q2: 什么场景下选择VARCHAR?A:存储变长数据(如:名称、地址、评论等)列的最大长度比平均长度大很多列很少被更新使用UTF-8等多字节字符集(节省空间)Q3: 两者在性能上有什么差异?A:CHAR:写入性能稍好(不需要计算长度)读取性能稍好(固定长度寻址更快)适合频繁更新的场景(减少碎片)VARCHAR:空间利用率高(适合大量数据)对于非常长的文本比CHAR更高效表更新时可能产生碎片,需要定期优化扩展知识存储示例输入CHAR(10)存储实际占用VARCHAR(10)存储实际占用‘Hello’'Hello ’10字节‘Hello’6字节(5+1)‘Hi’'Hi ’10字节‘Hi’3字节(2+1)‘HelloWorld’‘HelloWorld’10字节‘HelloWorld’11字节(10+1)CHAR和VARCHAR在不同数据库中的实现差异MySQL: CHAR最大255字符,VARCHAR最大65535字节SQL Server: CHAR最大8000字节,VARCHAR最大8000字节,VARCHAR(MAX)最大2GBOracle: CHAR最大2000字节,VARCHAR2最大4000字节实际应用示例场景一:用户信息表CREATE TABLE users ( id INT PRIMARY KEY, username VARCHAR(50), -- 用户名变长 gender CHAR(1), -- M或F,固定长度 phone CHAR(11), -- 手机号,固定长度 address VARCHAR(200) -- 地址变长 ); 场景二:产品编码表CREATE TABLE products ( product_code CHAR(8), -- 固定长度产品编码 product_name VARCHAR(100), -- 变长产品名称 description VARCHAR(1000) -- 变长描述 ); 总结CHAR适合固定长度、频繁更新的短字符串VARCHAR适合变长字符串,节省存储空间选择取决于数据特性、访问模式和存储需求性能优化需考虑存储空间和访问效率的平衡面试技巧先说明基本区别(固定长度vs可变长度)描述各自的优缺点和适用场景结合实际例子说明选择依据提到不同数据库的实现差异展示深度
  • [技术干货] 关系型数据库和非关系型数据库的区别
    问题描述• 这是数据库领域中比较基础的面试题。• 面试官可能会通过这个问题考察你对数据库基础知识的理解,并根据二者的特性进行进一步追问。核心答案1. 数据存储方式▫ 关系型:以表格形式存储,数据之间有关联关系。▫ 非关系型:以键值对、文档、列族等形式存储,更灵活。2. 数据结构▫ 关系型:固定的表结构,需要预先定义 schema。▫ 非关系型:灵活的数据结构,可以动态调整。3. 扩展性▫ 关系型:垂直扩展(增加服务器性能)。▫ 非关系型:水平扩展(增加服务器数量)。详细解析关系型数据库的特点• 常见产品有:MySQL、Oracle、PostgreSQL。• 主要优势:强一致性、支持复杂查询、事务完善。• 缺点:扩展性受限、处理大数据量时性能下降、结构固定不够灵活。非关系型数据库的特点• 主要产品有:Redis、MongoDB 等。• 主要优势:高扩展性、高性能、灵活的数据模型。• 缺点:一致性较弱、复杂查询支持有限、事务支持不完善。常见追问Q1:什么场景下选择关系型数据库?
A:需要强一致性的业务(如银行交易)、需要复杂查询的场景、数据结构相对固定的应用。Q2:什么场景下选择非关系型数据库?
A:需要处理大量数据、需要快速读写、数据结构经常变化的场景。
  • [问题求助] 基于redis实现限流的方式有几种?
    基于redis实现限流的方式有几种?哪种更好一点?
  • [问题求助] redis中的key过期了会立马删除吗?
    redis中的key过期了会立马删除吗?
  • [技术干货] centos7部署redis以及多实例
    安装redis下载wget http://download.redis.io/releases/redis-5.0.5.tar.gz解压到安装目录mkdir /usr/local/redis tar -zxvf redis-5.0.5.tar.gz -C /usr/local/redis/ 进入redis解压目录cd /usr/local/redis/redis-5.0.5编译make进入redis/src下cd /usr/local/redis/redis-5.0.5/src执行安装make install修改配置文件(后台启动,设置密码,外部访问)修改主配置文件 (1) 注释掉 bind 127.0.0.1 这一行(解决只能特定网段连接的限制) (2) 将 protected-mode 属性改为 no (关闭保护模式,不然会阻止远程访问) (3) 将 daemonize 属性改为 yes (这样启动时就在后台启动) (4) 设置密码 搜索 requirepass foobared 添加 requirepass 你设置的密码然后开放6379端口启动,停止和重启redis-server /usr/local/redis/redis-5.0.5/redis.conf连接redisredis-cliauth 密码redis多实例部署一台服务器上部署多个redis使用。首先找到redis文件夹下,将redis.conf复制到自己找到的目录下,这边我是直接创建了个bin目录。如果有几个实例那么久复制几份其次就改配置文件,具体要修改的地方是==1、bind==在默认情况下,bind监听的地址为127.0.0.1,因此,我们在新的配置文件中,必须要将bind监听的地址修改为本机的IP地址。==2、daemonize==在Redis多实例场景下,我们需要Redis的启动命令而不是启动脚本来启动新的Redis实例,因此,我们必须要将该参数改为yes,使得Redis后台启动。==3、port==在计算机中,不可能存在多个进程共同监听同一个端口,否则会出现端口已被占用的错误,因此,我们必须修改新的Redis实例的监听端口。==4、pidfile==pidfile也必须进行修改,否则会与原来的实例的pid文件名称相同,造成错误。==5、logfile==与pidfile类似,我们也必须修改logfile,即Redis的日志文件。==6、dir==同样的,我们也必须修改Redis的持久化存储目录。:warning:这里注意,如果配置了多实例那么启动的时候要带配置文件去启动redis-server /usr/local/redis/redis-5.0.5/bin/redis6381.conf 关闭的时候要杀死进程netstat -lntp或者还有一个操作ps -aux|grep redis进入服务端的命令redis-cli -p 6381 -a 你的密码
  • [技术干货] springboot整合redis过期key监听实现订单过期操作
    业务场景说明对于订单问题,那些下单了但是没有去支付的(占单情况),不管对于支付宝还是微信都有订单的过期时间设置,但是对于我们自己维护的订单呢。两种方案:被动修改,主动修改。这里仅仅说明对于主动修改的监听实现修改redis的配置文件redis.conf(好像不改也可以)K:keyspace事件,事件以__keyspace@<db>__为前缀进行发布; E:keyevent事件,事件以__keyevent@<db>__为前缀进行发布; g:一般性的,非特定类型的命令,比如del,expire,rename等; $:字符串特定命令; l:列表特定命令; s:集合特定命令; h:哈希特定命令; z:有序集合特定命令; x:过期事件,当某个键过期并删除时会产生该事件; e:驱逐事件,当某个键因maxmemore策略而被删除时,产生该事件; A:g$lshzxe的别名,因此”AKE”意味着所有事件pom依赖坐标的引入<!--版本号说明。这里我使用的是<jedis.version>2.9.3</jedis.version>,<spring.boot.version>2.3.0.RELEASE</spring.boot.version>--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>${jedis.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring.boot.version}</version> </dependency> 这里可能会存在的依赖冲突问题是与io.nettyRedisConfig的配置package test.bo.work.config.redis; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import test.bo.work.util.StringUtil; /** * @author xiaobo */ @Configuration @EnableAutoConfiguration public class JedisConfig { private static final Logger LOGGER = LoggerFactory.getLogger(JedisConfig.class); @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.jedis.pool.max-active}") private int maxActive; @Value("${spring.redis.jedis.pool.max-wait}") private int maxWait; @Value("${spring.redis.jedis.pool.max-idle}") private int maxIdle; @Value("${spring.redis.jedis.pool.min-idle}") private int minIdle; @Bean public JedisPool redisPoolFactory() { try { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMaxWaitMillis(maxWait); jedisPoolConfig.setMaxTotal(maxActive); jedisPoolConfig.setMinIdle(minIdle); // JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password); String pwd = StringUtil.isBlank(password) ? null : password; JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, pwd,6); LOGGER.info("初始化Redis连接池JedisPool成功!地址: " + host + ":" + port); return jedisPool; } catch (Exception e) { LOGGER.error("初始化Redis连接池JedisPool异常:" + e.getMessage()); } return null; } @Bean public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); return container; } } 监听器实现package test.bo.work.config.redis; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; /** * @author xiaobo */ @Component @Slf4j public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener { public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); log.info("Redis Key Expiration Listener has been initialized."); } @Override public void onMessage(Message message, byte[] pattern) { String expiredKey = message.toString(); log.error(expiredKey); // 判断是否是想要监听的过期key if (expiredKey.startsWith(KuoCaiConstants.ORDER_REDIS_PREFIX)) { // 根据过期redisKey获取订单号 String transactionOrderId = expiredKey.substring(KuoCaiConstants.ORDER_REDIS_PREFIX.length()); transactionOrderService.updateTransactionOrderStatusByRedis(Long.valueOf(transactionOrderId)); } } } 【注意】可能存在的问题(监听事件失效)自己自定义了库,如下代码 @Override protected void doRegister(RedisMessageListenerContainer listenerContainer) { // 针对db6进行监听 listenerContainer.addMessageListener(this, new PatternTopic("__keyevent@6__:expired")); } 这种情况,如果你存入的key不在db6,那么你就看不到监听触发事件(==这里并不是失效,只是说可能出现的你认为失效的情况==)使用了默认的连接工厂,但是配置文件中又没有相关定义首先解释一下,默认情况下,Spring Boot使用Jedis作为Redis客户端,并且会自动根据application.properties或application.yml配置文件中的spring.redis属性来创建连接工厂。如果没有指定这些属性,则会使用默认的localhost:6379作为Redis服务器地址。如果你想要连接到其他的Redis服务器,可以在配置文件中设置spring.redis.host和spring.redis.port属性来指定Redis服务器的地址和端口号。对于以上情况可以自定义工厂然后注入即可(上面的JedisConfig.java文件)@Bean public JedisConnectionFactory jedisConnectionFactory() { RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); redisStandaloneConfiguration.setHostName(host); redisStandaloneConfiguration.setDatabase(db); redisStandaloneConfiguration.setPassword(RedisPassword.of(password)); redisStandaloneConfiguration.setPort(port); return new JedisConnectionFactory(redisStandaloneConfiguration); } @Bean public RedisMessageListenerContainer container(JedisConnectionFactory jedisConnectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(jedisConnectionFactory); return container; } 【警告】存在的问题Redis的Key事件通知机制默认是异步的,即Redis会在Key过期时发送事件通知给所有监听者,但是不能保证监听者一定会及时接收到通知。如果您的应用程序需要在Key过期后立即处理相关操作,可能需要使用其他方式来实现。在Redis中,Key的过期时间只是一个近似的时间,它并不是精确的,因此不能保证过期时间到达时就一定会立即过期。如果您需要在Key过期后立即处理相关操作,建议您使用其他方式来实现,例如使用定时任务或轮询方式检查过期Key。在Redis中,Key的过期时间不能被取消或重置。如果您在设计时考虑到Key的过期时间可能需要修改,建议您使用其他方式来实现。当Redis中的Key被持久化到磁盘上时,过期时间可能会受到影响,因为过期时间的计算是基于系统时间的,如果系统时间发生变化,过期时间可能会出现不准确的情况。因此,建议您使用其他方式来处理需要精确过期时间的场景。
  • [技术干货] springboot整合redis及lua脚本实现接口限流
    接口限流说明接口限流是指在某些场景下,对某个接口的请求进行限制,以避免因请求过多而导致的系统负载过高、资源耗尽等问题。通常情况下,接口限流可以通过一定的算法来实现,比如令牌桶算法、漏桶算法、计数器算法等。这些算法可以根据接口的不同特点和业务需求,对请求进行限制和平滑处理,以达到系统资源的最优化利用。令牌桶算法令牌桶算法(Token Bucket Algorithm):令牌桶算法可以通过限制请求的速率,来保护系统免受突发流量的冲击。该算法将请求和令牌都存放在一个桶中,每个请求需要从桶中取出一个令牌,如果桶中没有令牌,则请求将被拒绝。该算法能够平滑限制请求的速率,避免系统被突发流量打垮。优点:能够平滑限制请求的速率,适合对流量进行平滑的限制。对于短时间内的流量突发,可以处理突发请求,保护系统不被打垮。缺点:实现相对较为复杂,需要维护令牌桶的状态。无法应对突发的大量请求。适用场景:对于流量比较稳定的系统,需要对请求进行平滑限制的场景。对于需要对请求进行按照一定速率限制的场景。漏桶算法漏桶算法(Leaky Bucket Algorithm):漏桶算法与令牌桶算法相似,也是通过限制请求的速率来保护系统免受突发流量的冲击。该算法将请求放入一个漏桶中,每个请求都需要占据一定的空间,如果漏桶已满,则请求将被拒绝。该算法能够平滑限制请求的速率,但无法应对突发流量。优点:能够平滑限制请求的速率,适合对流量进行平滑的限制。对于流量突发的情况,能够防止系统被过载。缺点:无法应对突发的大量请求。实现相对较为复杂,需要维护漏桶的状态。适用场景:对于需要对请求进行按照一定速率限制的场景。计数器算法计数器算法(Counting Algorithm):计数器算法是最简单的限流算法,通过对每个接口的请求数进行计数,并对其进行限制,来保护系统。该算法能够很好地限制请求的数量,但无法平滑限制请求的速率。优点:实现简单,易于实现。缺点:无法平滑限制请求的速率。无法应对突发的大量请求。适用场景:对于需要对请求进行简单计数的场景。对于不需要进行流量平滑限制的场景。滑动窗口算法滑动窗口算法(Sliding Window Algorithm):滑动窗口算法可以通过限制请求的速率,来保护系统免受突发流量的冲击。该算法将请求按照时间顺序放入一个固定大小的窗口中,如果窗口已满,则新的请求将被拒绝。该算法能够平滑限制请求的速率,但无法应对突发流量。优点:能够平滑限制请求的速率,适合对流量进行平滑的限制。对于短时间内的流量突发,可以处理突发请求,保护系统不被打垮。缺点:实现相对较为复杂,需要维护窗口的状态。无法应对长时间的流量突发。适用场景:对于流量比较稳定的系统,需要对请求进行平滑限制的场景。对于需要对请求进行按照一定速率限制的场景。实现基于令牌桶+redis进行接口限流这里我是基于令牌桶算法进行了变种,也就是针对不同的用户以及不同的方法在不同的时刻进行了限制,当然这个仅仅看个人业务:one::引入maven坐标<!--lua脚本--> <dependency> <groupId>org.luaj</groupId> <artifactId>luaj-jse</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> :two::定义接口限制注解package test.bo.work.redislimit.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author xiaobo * @date 2023/3/13 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { // 限制名称 String key(); // 次数 int limit(); // 秒 int seconds(); } :three::lua脚本实现local current = tonumber(redis.call('get', KEYS[1]) or '0') -- 判断是否还有令牌可用 if current + 1 > tonumber(ARGV[1]) then return 0 else redis.call('incrby', KEYS[1], 1) redis.call('expire', KEYS[1], ARGV[2]) return 1 end:four::引入lua脚本package test.bo.work.redislimit; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Files; /** * @author xiaobo * @date 2023/3/13 */ @Component public class RedisLuaScripts { @Value("classpath:/lua/ratelimit.lua") private Resource rateLimitScriptResource; public RedisScript<Long> getRateLimitScript() throws IOException { String script = new String(Files.readAllBytes(rateLimitScriptResource.getFile().toPath())); return new DefaultRedisScript<>(script, Long.class); } } :five:: 定义一个接口限制的AOPpackage test.bo.work.redislimit.assept; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import test.bo.work.config.exception.BusinessException; import test.bo.work.redislimit.RedisLuaScripts; import test.bo.work.redislimit.annotation.RateLimit; import javax.servlet.http.HttpServletRequest; import java.time.LocalDate; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * @author xiaobo * @date 2023/3/13 */ @Aspect @Component public class RateLimitAspect { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RedisLuaScripts redisLuaScripts; @Around("@annotation(rateLimit)") public Object checkRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { // 获取请求头中的AREA_TOKEN // 获取RequestAttributes RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); // 从获取RequestAttributes中获取HttpServletRequest的信息 HttpServletRequest request = (HttpServletRequest) requestAttributes .resolveReference(RequestAttributes.REFERENCE_REQUEST); String areaToken = request.getHeader("AREA_TOKEN"); // 获取注解上的参数 String key = rateLimit.key(); int limit = rateLimit.limit(); int seconds = rateLimit.seconds(); String redisKey = key + areaToken + "-" + LocalDate.now(); RedisScript<Long> script = redisLuaScripts.getRateLimitScript(); List<String> keys = Collections.singletonList(redisKey); List<String> args = Arrays.asList(String.valueOf(limit), String.valueOf(seconds)); Long result = redisTemplate.execute(script, keys, args.toArray()); if (result != null && result == 1) { return joinPoint.proceed(); } else { throw new BusinessException(500,"Rate limit exceeded for " + key); } } } :end::接口实现package test.bo.work.redislimit.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import test.bo.work.entity.OpResult; import test.bo.work.redislimit.annotation.RateLimit; /** * @author xiaobo * @date 2023/3/13 */ @RestController @Slf4j public class RateLimitController { @PostMapping("rateLimit") @RateLimit(key = "rateLimit", limit = 10, seconds = 1) public OpResult testRateLimit() { return OpResult.Ok("success"); } } 说明基于令牌桶算法的变种在正常情况下可以很好地限制接口的访问速率,但在短时间内突然出现大量请求的情况下,该算法可能会出现较大的误差。原因是在突发请求的情况下,桶内已经有很多令牌,但这些令牌并不能很快地被消耗,导致一些请求得到了允许,而另一些请求被拒绝。而漏桶算法则不会出现这个问题,因为它是基于固定速率漏水的方式进行限流,无论突发请求多少,都不会超出限制速率。:warning::经过jmeter测试确实出现了这样的情况如果是需要对大量用户进行限流,建议使用更高效的限流算法,比如漏桶算法,或基于漏桶算法的Token Bucket算法
  • [交流吐槽] 【话题交流】AI自动调优SQL靠谱吗?实际用过的来说说体验!
    现在很多平台,比如MCP集成的AI模块,支持SQL分析和自动调优。理论上它能减少人工优化成本,但实际效果到底如何?你有没有尝试过AI帮你优化慢SQL?准确率怎么样?有没有踩过坑?欢迎一起来聊聊“AI调优”这件事值不值得信!
  • [技术干货] Redis-Cluster 与 Redis 集群的技术大比拼
    前言在分布式数据库的世界中,Redis-Cluster 和传统 Redis 集群像两位拥有独特技能的战士,各自展现出强大的战斗力。今天,我们将一起踏上 Redis 分布式战场,解密 Redis-Cluster 与传统 Redis 集群的技术奥秘,揭开它们之间的差异之幕。概念与原理对比Redis-Cluster:基于哈希槽的分布式解决方案Redis-Cluster采用了一种先进的分布式数据分片方式,即通过哈希槽(Hash Slot)将整个数据集划分为16384个槽。每个节点负责一部分槽,通过哈希算法将数据映射到相应的槽上。这样,数据的分布在集群中更为均匀,同时提供了更高的可扩展性。在Redis-Cluster中,节点之间通过Gossip协议进行通信,实现了自动发现和节点管理。优势:数据分布均匀:通过哈希槽的方式,实现了数据的均匀分布,避免了热点问题。高可扩展性:方便地增加或减少节点,实现集群的动态扩缩容。自动发现与管理:节点之间通过Gossip协议进行通信,实现了自动发现和管理。传统 Redis 集群:主从架构下的数据分片方式传统的Redis集群采用主从架构,其中包括若干个主节点和它们的从节点。每个主节点负责一部分数据,而它的从节点则负责复制主节点的数据。这种方式下,数据的分片是通过主节点进行的,而从节点则用于提高系统的可用性和容错能力。优势:简单可靠:传统集群采用主从结构,相对简单可靠,容易理解和维护。数据备份:主从架构下,每个主节点都有对应的从节点,实现了数据的备份。不足:数据分布可能不均匀:如果某个主节点的数据集较大,可能导致该节点成为瓶颈,造成性能问题。不利于动态扩缩容:传统集群的扩缩容相对繁琐,需要手动处理节点的添加和移除。通过对比这两种分布式方案的原理,可以根据项目需求和特点选择更适合的方案。如果需要更高的可扩展性和自动化管理,Redis-Cluster是一个更为先进的选择。如果对于简单可靠的要求更为关键,传统Redis集群仍然是一个可靠的解决方案。搭建与配置的异同Redis-Cluster 搭建:哈希槽分配、节点配置等步骤在搭建Redis-Cluster时,需要进行以下步骤:哈希槽分配: 将整个数据集划分为16384个槽,每个槽由一个唯一的整数标识。这些槽会被均匀分配到集群中的各个节点上。节点配置: 配置每个节点的信息,包括节点的IP地址、端口号等。每个节点需要知道集群中其他节点的信息以便进行通信。节点启动: 启动各个节点,并使它们加入到集群中。节点之间通过Gossip协议进行通信,实现自动发现和管理。优势:动态扩缩容: 通过哈希槽的方式,实现了集群的动态扩缩容,方便增加或减少节点。传统 Redis 集群搭建:主从配置、数据分片策略等设置在搭建传统Redis集群时,主要涉及以下步骤:主从配置: 确定主节点和从节点,配置主从关系。每个主节点有对应的一个或多个从节点用于数据备份。数据分片策略: 制定数据分片策略,确定哪些数据由哪个主节点负责,以及从节点用于备份的数据。节点启动: 启动各个节点,使其形成集群。主节点负责处理读写请求,从节点用于数据备份和提高系统的可用性。不足:动态扩缩容相对繁琐: 传统Redis集群的扩缩容相对繁琐,需要手动处理节点的添加和移除。通过对比这两种搭建方式,可以看出Redis-Cluster在动态扩缩容方面更为灵活,而传统Redis集群则相对简单可靠。选择哪种方式要根据具体项目需求和团队的运维能力做出权衡。管理与维护的不同之处故障处理:Redis-Cluster 的故障转移机制与传统 Redis 集群的对比Redis-Cluster 故障处理:在Redis-Cluster中,当一个主节点发生故障时,会通过Raft协议或 Sentinel 哨兵机制自动进行故障转移。集群中的其他节点会选举一个新的主节点,然后自动更新槽的分配信息。这样,整个集群的状态得以恢复,而不需要人工干预。传统 Redis 集群故障处理:在传统Redis集群中,当一个主节点发生故障时,由其对应的一个从节点接管主节点的工作。其他从节点会选择一个新的主节点,然后进行数据同步。这个过程需要一定的时间,而且可能导致一小段时间内的服务不可用。动态扩缩容:Redis-Cluster 如何动态添加或移除节点,与传统集群的对比Redis-Cluster 动态扩缩容:在Redis-Cluster中,可以通过向集群添加新节点或从集群中移除节点来实现动态扩缩容。添加新节点时,集群会自动将哈希槽进行重新分配,保持数据的均匀分布。移除节点时,集群同样会重新分配哈希槽,确保数据不会丢失。传统 Redis 集群动态扩缩容:在传统Redis集群中,动态扩缩容相对繁琐。需要手动配置新的主节点和从节点,并确保数据的平衡。移除节点同样需要手动进行,并确保数据的备份和同步。通过对比这两方面的差异,可以看出Redis-Cluster在故障处理和动态扩缩容方面更为灵活和自动化,减轻了运维的负担。传统Redis集群则相对简单可靠,但需要更多手动操作。选择哪种方式要根据具体的项目需求和运维团队的技术水平做出权衡。性能优化的异同数据分布算法:Redis-Cluster 中的哈希槽算法与传统集群的数据分片对比Redis-Cluster 数据分布算法:在Redis-Cluster中,数据的分布是通过哈希槽(Hash Slot)算法实现的。每个槽有一个唯一的整数标识,整个数据集被划分为16384个槽。通过哈希算法将数据映射到相应的槽上,然后分配到集群中的各个节点。这种方式保证了数据在集群中的均匀分布,避免了热点问题。传统 Redis 集群数据分布算法:在传统Redis集群中,数据的分布是由主节点负责的。每个主节点负责一部分数据,并有对应的从节点进行数据备份。数据的分布由主节点的分片策略决定,通常是通过对 key 进行 hash 得到的哈希值来决定数据属于哪个分片。数据一致性:不同集群方案下的数据一致性保障Redis-Cluster 数据一致性:在Redis-Cluster中,由于采用了哈希槽算法,当节点发生故障时,只会影响部分槽的数据。通过Raft协议或 Sentinel 哨兵机制,集群可以自动进行故障转移,确保整个集群的数据一致性。在正常情况下,读写请求会路由到负责相应槽的节点,保证了数据的一致性。传统 Redis 集群数据一致性:在传统Redis集群中,当主节点发生故障时,会由从节点接管主节点的工作。这个过程需要一定的时间,而且可能导致一小段时间内的服务不可用。在这个过程中,数据的一致性可能会受到一定影响。通过对比这两方面的差异,可以看出Redis-Cluster在数据分布算法和数据一致性方面更为先进和可靠。传统Redis集群虽然简单可靠,但在一些大规模和高并发的场景下可能需要更多的优化和手动干预。选择哪种方式要根据具体的项目需求和对一致性的要求做出权衡。
  • [技术干货] 【MyBatis保姆级教程上】近万字从零开始手把手教你玩转数据库操作!配置+CRUD+日志+参数传递全解析-转载
    1.前言哈喽大家好吖,今天我们开始学习MyBatis,今天我们先学习如何用注解的方式完成代码的编写,下一篇博文在完成如何使用XML文件实现MyBatis的编程。无论你是想10分钟快速上手,还是彻底搞懂MyBatis的底层逻辑,无需任何MyBatis基础,我将从环境搭建、日志打印、参数绑定,一路带你手撕增删改查核心操作。2.正文官网:MyBatis中文网(广告有点多看着心刺挠~)2.1MyBatis与JDBCMyBatis 和 JDBC 是 Java 中操作数据库的两个不同层级的工具,它们之间既有继承关系,也有显著的差异:基础关系:JDBC(Java Database Connectivity)是 Java 提供的标准数据库访问接口,定义了操作数据库的通用 API(如 Connection、Statement、ResultSet),但需要开发者手动编写所有数据库操作代码。MyBatis 是基于 JDBC 的持久层框架,对 JDBC 进行了高级封装,简化了数据库操作流程,同时保留了直接编写 SQL 的灵活性。关系总结:MyBatis 底层依赖 JDBC 驱动与数据库交互,但通过封装和扩展,隐藏了 JDBC 的复杂性。特性 JDBC MyBatis代码复杂度 需要手动管理连接、语句、结果集,代码冗余。 自动管理资源(如连接、事务),减少样板代码。SQL 与代码耦合 SQL 嵌入 Java 代码中,难以维护。 SQL 与 Java 代码解耦,通过 XML 或注解配置。结果集映射 需手动遍历 ResultSet 并映射到对象。 自动将结果集映射到 Java 对象(ORM 特性)。动态 SQL 需手动拼接字符串,易出错且不安全。 支持动态 SQL 标签(如 <if>、<foreach>)。事务管理 需手动提交/回滚事务。 支持声明式事务管理(整合 Spring 时更灵活)。缓存机制 无内置缓存。 提供一级缓存(SqlSession 级别)和二级缓存。扩展性 直接控制底层,灵活性高但开发效率低。 通过插件机制扩展功能(如分页、日志)。2.2MyBatis入门2.2.1准备工作配置项目用到的jar包:sql测试代码:-- 创建数据库DROP DATABASE IF EXISTS mybatis_test; CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4; -- 使用数据数据USE mybatis_test; -- 创建表[用户表]DROP TABLE IF EXISTS user_info;CREATE TABLE `user_info` (        `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,        `username` VARCHAR ( 127 ) NOT NULL,        `password` VARCHAR ( 127 ) NOT NULL,        `age` TINYINT ( 4 ) NOT NULL,        `gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2-女 0-默认',        `phone` VARCHAR ( 15 ) DEFAULT NULL,        `delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',        `create_time` DATETIME DEFAULT now(),        `update_time` DATETIME DEFAULT now() ON UPDATE now(),        PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;  -- 添加用户信息INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )VALUES ( 'admin', 'admin', 18, 1, '18612340001' );INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );  -- 创建文章表DROP TABLE IF EXISTS article_info;        CREATE TABLE article_info (        id INT PRIMARY KEY auto_increment,        title VARCHAR ( 100 ) NOT NULL,        content TEXT NOT NULL,        uid INT NOT NULL,        delete_flag TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',        create_time DATETIME DEFAULT now(),        update_time DATETIME DEFAULT now() ) DEFAULT charset 'utf8mb4'; -- 插入测试数据INSERT INTO article_info ( title, content, uid ) VALUES ( 'Java', 'Java正文', 1 );AI写代码sql2.2.2配置数据库配置yml文件:spring:  application:    name: mybatis-demo  # 数据库配置  datasource:    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true    username: root    password: root    driver-class-name: com.mysql.cj.jdbc.DriverAI写代码sql2.2.3持久层代码编写代码规范:数据库字段:全部小写,单词之间使用_分割Java规范:属性使用小驼峰来表示 model包(采用注解的方式实现):@Mapperpublic interface UserInfoMapper {     @Select("select * from user_info")    List<UserInfo> selectAll();}AI写代码java运行mapper包:@Datapublic class UserInfo {    private Integer id;    private String username;    private String password;    private Integer age;    private Integer gender;    private String phone;    private Integer deleteFlag;    private Date createTime;    private Date updateTime;}AI写代码java运行2.2.4测试第一种测试方法(在Test文件夹写测试代码):在test文件下创建mapper包,再在mapper包中创建Test用于测试数据库数据:@SpringBootTestclass UserInfoMapperTest {     @Autowired    private UserInfoMapper userInfoMapper;     @Test    void selectAll() {        System.out.println(userInfoMapper.selectAll());    }}AI写代码java运行测试结果:  或者直接在test文件夹下原有的Tests文件进行测试:@SpringBootTestclass MybatisDemoApplicationTests { @Autowiredprivate ApplicationContext context;//在测试代码中直接注入 @Testvoid contextLoads() {UserInfoMapper bean = context.getBean(UserInfoMapper.class);bean.selectAll().stream().forEach(x-> System.out.println(x));} }AI写代码java运行也可以正常输出结果。 这里讲解两个新的注解:@Test 注解:作用:标记一个方法为单元测试方法,用于验证单个代码单元的逻辑(如一个方法、一个类)。特点:不依赖 Spring 上下文:测试方法独立运行,不启动 Spring 容器。轻量快速:适合测试纯 Java 代码逻辑,无需加载 Spring 相关组件。需手动处理依赖:如果被测试的类依赖其他组件(如 Service、Repository),需要手动 Mock(例如用 Mockito)。@SpringBootTest 注解:作用:标记一个测试类为集成测试,启动完整的 Spring 应用上下文,模拟真实运行环境。特点:加载完整 Spring 上下文:自动扫描配置、Bean、数据库、外部服务等。支持依赖注入:可以直接使用 @Autowired 注入 Bean。配置灵活:通过 webEnvironment 指定 Web 环境(如模拟 Servlet 环境、真实端口等)。较慢:因为加载整个应用,适合测试组件交互、API 接口等。特性 @Test @SpringBootTest测试类型 单元测试 集成测试Spring 上下文 不加载 加载完整 Spring 上下文依赖注入 不支持(需手动 Mock) 支持(通过 @Autowired)执行速度 快 较慢(因加载上下文)适用场景 验证纯 Java 逻辑 测试组件交互、数据库操作、API 接口第二种测试方法(通过Controller调用service中方法): 代码结构:userController:@RequestMapping("/user")@RestControllerpublic class UserInfoController {     @Autowired    private UserService userService;     @RequestMapping("/getAllUser")    public List<UserInfo> getAllUser(){        return userService.getAllUser();    }}AI写代码java运行userService:@Servicepublic class UserService {    @Autowired    private UserInfoMapper userInfoMapper;     public List<UserInfo> getAllUser() {        return userInfoMapper.selectAll();    }}AI写代码java运行运行DemoApplication,并访问网页:同样测试成功。此外,我们会还有第三种方法来写测试用例,即让编译器为我们生成:右键generate选择test出现:解释@Before与@After:一个测试方法前实现,一个测试方法后实现将二者勾选上后覆盖:@SpringBootTestclass UserInfoMapperTest {     @Autowired    private UserInfoMapper userInfoMapper;     @Test    void selectAll() {        System.out.println(userInfoMapper.selectAll());    }     @BeforeEach    void setUp() {        System.out.println("before.......");    }     @AfterEach    void tearDown() {        System.out.println("after......");    }}AI写代码java运行输出结果如下:至此,测试流程已讲解完毕,接下来来讲解MyBatis的基础操作。 2.3MyBatis基础操作2.3.1打印日志为了方便学习基础操作,很显然打印日志必不可少,所以我们先配置以下日志的输出格式:mybatis:  # 配置 mybatis xml 的文件路径,在 resources/mapper 创建所有表的 xml 文件  mapper-locations: classpath:mapper/**Mapper.xml  configuration:     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl    map-underscore-to-camel-case: true #配置驼峰自动转换AI写代码java运行输出测试日志:在日志中间部分:==>:表示向数据库输入的<==:表示数据库返回的2.3.2参数传递MyBatis 的参数传递规则围绕 方法参数名与 SQL 占位符的绑定,分为以下几种场景:1. 无参数方法@Select("SELECT * FROM user_info")List<UserInfo> selectAll();AI写代码java运行场景:无需传递参数。要点:直接执行 SQL,无需处理参数绑定。 2. 单参数方法案例 1:selectAllById1@Select("select * from user_info where id = #{id}")    UserInfo selectAllById1(Integer id);AI写代码java运行案例 2:selectAllById2@Select("select * from user_info where id = #{id}")    List<UserInfo> selectAllById2(Integer id);AI写代码java运行参数规则:单参数时:占位符 #{xxx} 的 xxx 可以任意命名(如 #{id}、#{value}、#{abc} 均可)。MyBatis 默认按参数顺序绑定,与占位符名称无关。但为了可读性,建议占位符名称与参数名一致。返回值差异:selectAllById1:返回单个对象(查询结果最多一条时)。selectAllById2:返回列表(即使结果只有一条)。 3. 多参数方法@Select("select * from user_info where username = #{username} and password = #{password}")    List<UserInfo> selectByNameAndPassword(String username, String password);AI写代码java运行参数规则:默认行为(无 @Param 注解):MyBatis 会将多个参数封装为 Map,键为 param1, param2, ... 或 arg0, arg1, ...。此时 SQL 占位符必须使用 {param1}、#{param2} 或 #{arg0}、#{arg1}:直接使用参数名(如 #{username})会报错,因为默认无法解析参数名。推荐做法(使用 @Param 注解):@Select("SELECT * FROM user_info WHERE username = #{username} AND password = #{password}")List<UserInfo> selectByNameAndPassword(    @Param("username") String username,     @Param("password") String password);AI写代码java运行通过 @Param 显式指定参数名,占位符 #{xxx} 必须与注解值一致。本质:MyBatis 会将参数封装为 Map,键为 @Param 指定的名称。MyBatis 的参数绑定最终是 基于键值对的 Map 结构:单参数:键可以是任意名称,或通过 @Param 指定。多参数:必须通过 @Param 指定键,否则只能用 param1/arg0 等默认键。 这里最后对比下不同的参数传递:场景 占位符写法示例 是否需要 @Param 注意事项无参数 无 否 直接执行 SQL单参数 #{任意名称} 否(建议与参数名一致) 占位符名称无约束,但需保持语义清晰多参数(默认) #{param1}, #{arg0} 否 可读性差,易出错多参数(推荐) #{自定义名称} 是 需通过 @Param 指定名称2.3.3增测试代码:    @Options(useGeneratedKeys = true, keyProperty = "id")    @Insert("insert into user_info(username, password, age) values (#{username}, #{password}, #{age})")    Integer insertUser(UserInfo userInfo);AI写代码java运行1. 核心功能作用:向 user_info 表插入一条用户记录,并回填数据库生成的自增主键到 UserInfo 对象的 id 属性。返回值:返回插入操作影响的行数(通常为 1)。2. 注解解析(1) @Insert 注解定义 SQL:指定插入语句,使用 #{属性名} 占位符绑定参数。#{username}、#{password}、#{age} 会调用 UserInfo 对象的 getUsername()、getPassword()、getAge() 方法获取值。要求:UserInfo 类必须有对应的 getter 方法。(2) @Options 注解用途:控制 SQL 执行的附加选项。关键参数:useGeneratedKeys = true:启用数据库自增主键回填。keyProperty = "id":将生成的主键值回填到 UserInfo 对象的 id 属性中。要求:数据库表的主键字段必须支持自增(如 MySQL 的 AUTO_INCREMENT)。UserInfo 类必须有 id 属性的 setter 方法(即 setId)。2.3.4删(简)delete from user_info where id=6AI写代码sql把SQL中的常量替换为动态的参数Mapper接口:@Delete("delete from user_info where id = #{id}")void delete(Integer id);AI写代码java运行2.3.5改(简)update user_info set username="zhaoliu" where id=5AI写代码sql把SQL中的常量替换为动态的参数Mapper接口:@Update("update user_info set username=#{username} where id=#{id}")void update(UserInfo userInfo);AI写代码java运行2.3.6查我们前文讲解参数传递的时候牵扯到一些查询操作,但难免会遇到比如代码中的参数名与表中的参数名不一致的情况接下来我们介绍三种解决该问题的方式。2.3.6.1起别名第一个方案这里我们就需要用到别名来进行操作了:@Select("select id, username,`password`, age, gender,  phone, " +            "delete_flag as deleteFlag, create_time as createTime, update_time as updateTime" +            " from user_info")     List<UserInfo> selectAll();AI写代码java运行2.3.6.2结果映射 当然,上面那个代码显得十分臃肿,这时又有注解来给咱们减轻压力了:只要告诉注解,哪个参数和哪个参数是一一对应即可。@Results(id = "BaseMap", value = {            @Result(column = "delete_flag", property = "deleteFlag"),            @Result(column = "create_time", property = "createTime"),            @Result(column = "update_time", property = "updateTime")    })    @Select("select * from user_info")    List<UserInfo> selectAll();AI写代码java运行正常运行~核心思路解析:@Results 注解的作用与使用:1. @Results 注解的作用定义结果映射规则:将查询结果中的数据库字段(column)映射到 Java 对象的属性(property)。解决命名差异:当数据库字段名与 Java 属性名不一致时(如蛇形命名 delete_flag vs 驼峰命名 deleteFlag),需手动指定映射关系。复用映射配置:通过 id = "BaseMap" 标记此映射规则,可在其他方法中通过 @ResultMap("BaseMap") 复用。2. 关键注解解析注解/属性 说明@Results 定义一组结果映射规则,包含多个 @Result。id = "BaseMap" 为此映射规则命名,方便其他方法通过 @ResultMap 引用。@Result 定义单个字段的映射关系。column = "delete_flag" 数据库字段名(实际查询结果中的列名)。property = "deleteFlag" Java 实体类属性名(UserInfo 类中的属性)。3. 执行流程执行查询:调用 selectAll() 方法,MyBatis 执行 SQL 查询。结果映射:根据 @Results 定义的规则,将结果集中的 delete_flag 映射到 UserInfo.deleteFlag。同理处理 create_time → createTime、update_time → updateTime。返回对象:将映射后的 UserInfo 对象装入列表返回。2.3.6.3开启驼峰命名 当然我们可以在yml文件中配置使代码更加便捷:mybatis:  # 配置 mybatis xml 的文件路径,在 resources/mapper 创建所有表的 xml 文件  configuration: # 配置打印 MyBatis日志    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl    map-underscore-to-camel-case: true #配置驼峰自动转换AI写代码java运行当然想使用这个配置,你的变量的命名必须严格按照小驼峰命名法(代码就很简单啦)。@Select("select * from user_info")    List<UserInfo> selectAll();AI写代码java运行正常输出:3.小结今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,需要所有的源代码可以去我的gitee上就可以啦~你的支持就是对我最大的鼓励,大家加油!————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/2301_81073317/article/details/148234380
  • [技术干货] 【技术合集】2025年5月数据库技术合集
    这一组文章聚焦于数据库智能化与 Redis 高可用实践,展现了从智能体自动生成 SQL 到 Redis 高并发问题应对的全流程能力。文章《从此,SQL 不写一行,智能体全帮我搞定!》介绍了基于 AI 智能体实现自动 SQL 生成与数据操作的技术趋势,展望了无代码或低代码数据库操作的未来场景。接下来的几篇 Redis 相关技术干货依次揭示了事务机制的工作原理与使用场景(如 MULTI、EXEC)、缓存雪崩产生的原因与系统保护策略(例如设置合理过期时间、加锁等)、以及如何防止缓存击穿,例如使用互斥锁和热点数据预加载等方式,为高并发业务场景下的缓存架构设计提供了详尽参考。这些内容不仅提升了开发者在数据库优化和架构防护方面的认知,也为实际项目中的高可用设计提供了理论支持与落地方案。⸻📚 标题与链接列表:1. 标题: 从此,SQL 不写一行,智能体全帮我搞定!链接: https://bbs.huaweicloud.com/forum/thread-0259183184570939155-1-1.html2. 标题: Redis事务悄然而至:命令的背后故事链接: https://bbs.huaweicloud.com/forum/thread-0275183567368800179-1-1.html3. 标题: Redis缓存雪崩:预防、应对和解决方案链接: https://bbs.huaweicloud.com/forum/thread-0261183567445363103-1-1.html4. 标题: Redis缓存保卫战:拒绝缓存击穿的进攻链接: https://bbs.huaweicloud.com/forum/thread-0278183567506595166-1-1.html
  • Redisson 实现的分布式锁相对于 SETNX 的核心优势对比
    Redisson 实现的分布式锁相对于 SETNX 的核心优势在于​​原子性保障、功能扩展性、可靠性及开发友好性​​。以下是具体对比分析:​​一、核心优势对比​​​​特性​​​​SETNX 实现​​​​Redisson 实现​​​​优势说明​​​​原子性操作​​需组合 SETNX + EXPIRE 命令,非原子性操作通过 Lua 脚本实现加锁、续期、释放锁的原子性避免竞态条件(如锁超时后业务未完成导致锁失效)^1,6​​自动续期(看门狗)​​需手动实现后台线程续期内置看门狗机制,自动延长锁有效期(默认 30 秒续期至 30 秒)防止长事务因锁超时导致的并发问题^3,4​​可重入性​​需自行维护线程标识和计数器默认支持可重入锁,通过 Hash 结构记录线程 ID 和重入次数支持递归调用或嵌套锁场景^2,5​​锁释放安全性​​需通过 Lua 脚本校验锁标识,否则可能误删其他线程的锁自动校验锁持有者身份,仅允许持有者释放锁避免误删锁引发的并发问题^6,8​​高可用性​​单节点 Redis 存在单点故障风险支持 RedLock 算法(多节点集群)和哨兵模式提升锁服务的容灾能力(@ref)​​开发复杂度​​需手动实现锁续期、可重入、异常处理等逻辑提供 RLock 接口和丰富 API(如 tryLock、lockInterruptibly)简化代码,降低维护成本^3,7​​二、Redisson 的核心优势解析​​​​1. 原子性保障​​​​Lua 脚本封装​​:Redisson 将加锁、续期、释放锁等操作封装为原子性 Lua 脚本,避免多命令组合的非原子风险。-- 加锁 Lua 脚本(简化版)if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return nilelse redis.call('pexpire', KEYS[1], ARGV[2]) return 0end该脚本确保​​判断锁状态、更新计数器、设置过期时间​​的原子性^6,8。​​2. 自动续期机制​​​​看门狗线程​​:后台线程定期检查锁的剩余存活时间,若小于阈值(如 10 秒),则通过 EXPIRE 命令续期。​​动态调整​​:续期间隔根据锁的剩余时间动态计算,避免频繁续期带来的性能损耗^3,4。​​3. 可重入性支持​​​​数据结构​​:使用 Redis Hash 存储锁标识(如 lockName: { "threadId": 重入次数 })。​​重入逻辑​​:同一线程再次获取锁时,直接增加计数器,无需重新竞争锁^2,5。​​4. 高可用方案​​​​RedLock 算法​​:通过多个独立 Redis 实例实现分布式锁,需超过半数节点成功加锁。​​哨兵模式​​:自动切换故障节点,保障锁服务的高可用性(@ref)。​​5. 丰富的锁类型​​​​公平锁​​:按请求顺序分配锁,避免饥饿现象。​​联锁(MultiLock)​​:同时锁定多个锁,保证原子性。​​红锁(RedLock)​​:基于多节点集群的强一致性锁^7,9。​​三、适用场景对比​​​​场景​​​​SETNX 适用性​​​​Redisson 适用性​​简单库存扣减✅ 适合(短时操作,无需复杂功能)✅ 适合(但需自行处理续期)长事务处理(>30 秒)❌ 高风险(需手动续期)✅ 推荐(自动续期保障)递归调用或嵌套锁❌ 需自行实现可重入逻辑✅ 原生支持高并发读写锁分离❌ 需自行实现读写锁逻辑✅ 提供 RReadWriteLock多节点集群环境❌ 需自行实现 RedLock✅ 原生支持 RedLock 和哨兵模式​​四、性能对比​​​​单节点模式​​:SETNX 方案 TPS 约 ​​35,000 次/秒​​(轻量级操作)。Redisson 方案 TPS 约 ​​28,000 次/秒​​(因 Hash 操作和后台线程开销)(@ref)。​​集群模式​​:Redisson 的 RedLock 在高可用场景下性能更优,且支持动态扩缩容。​​五、总结:为何选择 Redisson?​​​​开发效率​​:提供完整 API 和自动续期、可重入等高级功能,减少编码复杂度。​​可靠性​​:通过原子性操作和看门狗机制规避单点故障和锁超时风险。​​扩展性​​:支持公平锁、联锁、红锁等复杂场景,适应企业级需求。​​社区支持​​:Redis 官方推荐方案,生态完善,问题修复及时。​​适用建议​​:若业务简单且对性能敏感(如秒杀库存),可优化 SETNX 实现。若涉及长事务、可重入性或高可用需求,优先选择 Redisson。
  • RedLock分布式锁算法
    ​​1. 什么是 RedLock?​​RedLock 是 Redis 官方提出的一种​​分布式锁算法​​,由 Redis 作者 Salvatore Sanfilippo(Antirez)设计,旨在解决单点 Redis 实例作为分布式锁时的​​单点故障问题​​。其核心思想是通过​​多节点投票机制​​实现锁的强一致性:​​多节点加锁​​:客户端需在多个独立 Redis 实例(通常为奇数个,如 5 个)上同时尝试获取锁。​​多数派确认​​:只有当超过半数节点(如 5 个节点中的 3 个)成功加锁时,才认为锁获取成功。​​容错性​​:即使部分节点故障,只要多数节点存活,锁服务仍可用。​​2. RedLock 解决了什么问题?​​RedLock 主要解决以下问题:​​单点故障​​:传统单节点 Redis 锁在主节点宕机时会导致锁失效,而 RedLock 通过多节点冗余避免这一问题。​​主从同步延迟​​:在 Redis 主从架构中,主节点加锁后若未同步到从节点即宕机,从节点晋升为主节点可能导致锁丢失。RedLock 通过多节点确认机制规避此风险。​​高可用性​​:即使部分节点故障,锁服务仍能继续运行。​​3. Redisson 中为何废弃 RedLock?​​尽管 RedLock 设计初衷良好,但存在以下关键问题,导致 Redisson 官方废弃:​​性能瓶颈​​​​网络延迟​​:需多次与多节点交互,加锁耗时增加,尤其在高延迟网络中表现更差。​​竞争开销​​:多数派机制要求等待多数节点响应,加锁成功率受网络和节点负载影响。​​并发安全性争议​​​​时钟依赖​​:RedLock 依赖系统时钟判断锁超时,若时钟发生跳跃(如 NTP 同步),可能导致锁提前释放。​​GC 停顿风险​​:客户端垃圾回收(GC)停顿可能导致锁超时,其他客户端获取锁后,原客户端误判锁仍有效,引发并发问题。​​实现复杂度高​​​​节点管理​​:需手动维护多个独立 Redis 实例,且需确保密钥分散在不同节点,增加运维成本。​​争议性设计​​:社区对 RedLock 的安全性存在分歧(如 Martin Kleppmann 与 Antirez 的争论),且无完美解决方案。​​替代方案更优​​​​ZooKeeper/etcd​​:基于强一致性的原生分布式锁实现,更适合高并发场景。​​Redis 集群 + 看门狗​​:通过 Redis 集群和自动续期机制(如 Redisson 的 Watch Dog)提升可靠性,简化实现。​​总结​​​​RedLock 核心价值​​:通过多节点投票机制解决单点故障问题,提升锁的可用性。​​废弃原因​​:性能、安全性争议及复杂度高,且已有更优替代方案(如 ZooKeeper、Redis 集群)。​​Redisson 的替代方案​​:推荐使用 RLock(基于单节点 + 看门狗)或集群化 Redis 实现分布式锁。
  • 什么是Redisson 的看门狗机
    Redisson 的​​看门狗机制(Watch Dog)​​是其分布式锁(如 RLock)的核心特性之一,用于解决​​锁自动过期导致业务未完成锁失效​​的问题。它通过后台线程动态延长锁的持有时间,确保业务逻辑执行期间锁不会意外释放。​​一、为什么需要看门狗机制?​​在分布式系统中,如果客户端获取锁后,业务逻辑执行时间超过了锁的预设过期时间(如 30 秒),锁会自动释放。此时其他客户端可能获取到锁,导致​​并发安全问题​​(如数据覆盖)。​​看门狗的作用​​:在锁即将过期时,自动续期(延长锁的过期时间),确保业务逻辑执行完成前锁始终有效。​​二、看门狗机制的核心原理​​​​1. 锁的初始设置​​当客户端通过 lock.lock() 获取锁时,Redisson 会执行以下 Redis 命令:SET lock_key unique_value NX EX 30NX:仅当键不存在时设置锁。EX 30:锁的初始过期时间为 30 秒。unique_value:唯一标识(如 UUID + 线程 ID),用于确保只有锁持有者能释放锁。​​2. 看门狗的触发​​​​续期条件​​:当锁的剩余存活时间小于等于 ​​看门狗的默认阈值(如 1/3 锁过期时间)​​ 时,触发续期。例如,锁初始过期时间为 30 秒,当剩余时间 ≤ 10 秒时,看门狗开始工作。​​续期频率​​:默认每隔 ​​1/3 锁过期时间​​ 续期一次(如 30 秒锁每隔 10 秒续期)。​​3. 续期操作​​看门狗会通过 Redis 的 EXPIRE 命令,将锁的过期时间​​重置为初始值​​(如再次设置为 30 秒)。EXPIRE lock_key 30​​续期次数无限制​​:只要锁未被释放,看门狗会持续续期。​​4. 锁释放​​当调用 lock.unlock() 时,Redisson 会通过 Lua 脚本验证 unique_value,确保只有锁持有者能释放锁:if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then return redis.call('del', KEYS[1])else return 0end释放锁后,看门狗线程终止。​​三、看门狗的实现细节​​​​1. 看门狗线程​​Redisson 内部维护一个 ScheduledExecutorService 线程池,用于执行锁续期任务。每个锁对应一个独立的续期任务。​​2. 动态调整续期间隔​​续期间隔根据锁的初始过期时间动态计算。例如:初始过期时间 leaseTime = 30,000 ms。看门狗触发间隔为 leaseTime / 3(约 10 秒)。​​3. 续期逻辑伪代码​​class WatchDog extends Thread { private final RLock lock; private long leaseTime; // 初始过期时间(如30秒) private long lastRenewTime; public void run() { while (isLocked()) { long currentTime = System.currentTimeMillis(); // 检查是否需要续期(剩余时间 ≤ leaseTime / 3) if (currentTime - lastRenewTime >= leaseTime / 3) { renewLock(); // 调用 EXPIRE 命令续期 lastRenewTime = currentTime; } Thread.sleep(100); // 适当休眠避免频繁检查 } } private void renewLock() { // 发送 EXPIRE 命令重置锁过期时间 redis.expire(lockKey, leaseTime); }}​​四、看门狗的优缺点​​​​优点​​​​避免业务未完成锁失效​​:确保长耗时任务执行期间锁始终有效。​​无感知续期​​:开发者无需手动管理锁续期。​​高可靠性​​:通过唯一标识(unique_value)和 Lua 脚本保证原子性。​​缺点​​​​潜在性能开销​​:续期操作会增加 Redis 请求频率。​​客户端崩溃风险​​:如果客户端崩溃且未释放锁,看门狗会持续续期,导致锁永久占用(需依赖超时机制或手动干预)。​​五、配置与调优​​​​1. 自定义锁过期时间​​可以通过 lock.lock(leaseTime, timeUnit) 指定锁的初始过期时间:// 设置锁过期时间为 60 秒lock.lock(60, TimeUnit.SECONDS);​​2. 关闭看门狗​​不推荐关闭,但可通过设置 leaseTime 为 -1 禁用自动续期:// 锁永不自动续期(需手动释放)lock.lock(-1, TimeUnit.SECONDS);​​3. 调整续期间隔​​通过修改 Redisson 配置调整续期频率(需源码定制)。​​六、典型应用场景​​​​长事务处理​​:如订单支付、批量数据同步。​​微服务分布式锁​​:确保跨服务资源一致性。​​定时任务防并发​​:避免多个节点同时执行同一任务。​​七、总结​​Redisson 的看门狗机制通过​​动态续期​​解决了分布式锁因业务耗时过长导致的失效问题,其核心是:基于 Redis 的 EXPIRE 命令实现锁续期。通过后台线程监控锁状态并自动续期。结合唯一标识和 Lua 脚本保证安全性。​​使用建议​​:默认开启看门狗,合理设置锁的初始过期时间。在业务逻辑中确保及时释放锁(调用 unlock())。对于极高并发场景,可结合 Redisson 的看门狗日志监控性能开销。
总条数:553 到第
上滑加载中