分布式锁—Redisson的同步器组件

1.Redisson的分布式锁简单总结

Redisson分布式锁包括:可重入锁、公平锁、联锁、红锁、读写锁。

(1)可重入锁RedissonLock

非公平锁,最基础的分布式锁,最常用的锁。

(2)公平锁RedissonFairLock

各个客户端尝试获取锁时会排队,按照队列的顺序先后获取锁。

(3)联锁MultiLock

可以一次性加多把锁,从而实现一次性锁多个资源。

(4)红锁RedLock

RedLock相当于一把锁。虽然利用了MultiLock包裹了多个小锁,但这些小锁并不对应多个资源,而是每个小锁的key对应一个Redis实例。只要大多数的Redis实例加锁成功,就可以认为RedLock加锁成功。RedLock的健壮性要比其他普通锁要好。

但是RedLock也有一些场景无法保证正确性,当然RedLock只要求部署主库。比如客户端A尝试向5个Master实例加锁,但仅仅在3个Maste中加锁成功。不幸的是此时3个Master中有1个Master突然宕机了,而且锁key还没同步到该宕机Master的Slave上,此时Salve切换为Master。于是在这5个Master中,由于其中有一个是新切换过来的Master,所以只有2个Master是有客户端A加锁的数据,另外3个Master是没有锁的。但继续不幸的是,此时客户端B来加锁,那么客户端B就很有可能成功在没有锁数据的3个Master上加到锁,从而满足了过半数加锁的要求,最后也完成了加锁,依然发生重复加锁。

(5)读写锁之读锁RedissonReadLock和写锁RedissonWriteLock

不同客户端线程的四种加锁情况:

情况一:先加读锁再加读锁,不互斥

情况二:先加读锁再加写锁,互斥

情况三:先加写锁再加读锁,互斥

情况四:先加写锁再加写锁,互斥

同一个客户端线程的四种加锁情况:

情况一:先加读锁再加读锁,不互斥

情况二:先加读锁再加写锁,互斥

情况三:先加写锁再加读锁,不互斥

情况四:先加写锁再加写锁,不互斥

2.Redisson的Semaphore简介

(1)Redisson的Semaphore原理图

Semaphore也是Redisson支持的一种同步组件。Semaphore作为一个锁机制,可以允许多个线程同时获取一把锁。任何一个线程释放锁之后,其他等待的线程就可以尝试继续获取锁。

(2)Redisson的Semaphore使用演示

public class RedissonDemo {public static void main(String[] args) throws Exception {//连接3主3从的Redis CLusterConfig config = new Config();...//SemaphoreRedissonClient redisson = Redisson.create(config);final RSemaphore semaphore = redisson.getSemaphore("semaphore");semaphore.trySetPermits(3);for (int i = 0; i < 10; i++) {new Thread(new Runnable() {public void run() {try {System.out.println(new Date() + ":线程[" + Thread.currentThread().getName() + "]尝试获取Semaphore锁");semaphore.acquire();System.out.println(new Date() + ":线程[" + Thread.currentThread().getName() + "]成功获取到了Semaphore锁,开始工作");Thread.sleep(3000);semaphore.release();System.out.println(new Date() + ":线程[" + Thread.currentThread().getName() + "]释放Semaphore锁");} catch (Exception e) {e.printStackTrace();}}}).start();}}
}

3.Redisson的Semaphore源码剖析

(1)Semaphore的初始化

public class Redisson implements RedissonClient {//Redis的连接管理器,封装了一个Config实例protected final ConnectionManager connectionManager;//Redis的命令执行器,封装了一个ConnectionManager实例protected final CommandAsyncExecutor commandExecutor;...protected Redisson(Config config) {this.config = config;Config configCopy = new Config(config);//初始化Redis的连接管理器connectionManager = ConfigSupport.createConnectionManager(configCopy);...  //初始化Redis的命令执行器commandExecutor = new CommandSyncService(connectionManager, objectBuilder);...}@Overridepublic RSemaphore getSemaphore(String name) {return new RedissonSemaphore(commandExecutor, name);}...
}public class RedissonSemaphore extends RedissonExpirable implements RSemaphore {private final SemaphorePubSub semaphorePubSub;final CommandAsyncExecutor commandExecutor;public RedissonSemaphore(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.commandExecutor = commandExecutor;this.semaphorePubSub = commandExecutor.getConnectionManager().getSubscribeService().getSemaphorePubSub();}...
}

(2)Semaphore设置允许获取的锁数量

public class RedissonSemaphore extends RedissonExpirable implements RSemaphore {...@Overridepublic boolean trySetPermits(int permits) {return get(trySetPermitsAsync(permits));}@Overridepublic RFuture<Boolean> trySetPermitsAsync(int permits) {RFuture<Boolean> future = commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,//执行命令"get semaphore",获取到当前的数值"local value = redis.call('get', KEYS[1]); " +"if (value == false) then " +//然后执行命令"set semaphore 3"//设置这个信号量允许客户端同时获取锁的总数量为3"redis.call('set', KEYS[1], ARGV[1]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1;" +"end;" +"return 0;",Arrays.asList(getRawName(), getChannelName()),permits);if (log.isDebugEnabled()) {future.onComplete((r, e) -> {if (r) {log.debug("permits set, permits: {}, name: {}", permits, getName());} else {log.debug("unable to set permits, permits: {}, name: {}", permits, getName());}});}return future;}...
}

首先执行命令"get semaphore",获取到当前的数值。然后执行命令"set semaphore 3",也就是设置这个信号量允许客户端同时获取锁的总数量为3。

(3)客户端尝试获取Semaphore的锁

public class RedissonSemaphore extends RedissonExpirable implements RSemaphore {...private final SemaphorePubSub semaphorePubSub;final CommandAsyncExecutor commandExecutor;public RedissonSemaphore(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.commandExecutor = commandExecutor;this.semaphorePubSub = commandExecutor.getConnectionManager().getSubscribeService().getSemaphorePubSub();}@Overridepublic void acquire() throws InterruptedException {acquire(1);}@Overridepublic void acquire(int permits) throws InterruptedException {if (tryAcquire(permits)) {return;}CompletableFuture<RedissonLockEntry> future = subscribe();commandExecutor.syncSubscriptionInterrupted(future);try {while (true) {if (tryAcquire(permits)) {return;}//获取Redisson的Semaphore失败,于是便调用本地JDK的Semaphore的acquire()方法,此时当前线程会被阻塞//之后如果Redisson的Semaphore释放了锁,那么当前客户端便会通过监听订阅事件释放本地JDK的Semaphore,唤醒被阻塞的线程,继续执行while循环//注意:getLatch()返回的是JDK的Semaphore = "new Semaphore(0)" ==> (state - permits)//首先调用CommandAsyncService.getNow()方法//然后调用RedissonLockEntry.getLatch()方法//接着调用JDK的Semaphore的acquire()方法commandExecutor.getNow(future).getLatch().acquire();}} finally {unsubscribe(commandExecutor.getNow(future));}}@Overridepublic boolean tryAcquire(int permits) {//异步转同步return get(tryAcquireAsync(permits));}@Overridepublic RFuture<Boolean> tryAcquireAsync(int permits) {if (permits < 0) {throw new IllegalArgumentException("Permits amount can't be negative");}if (permits == 0) {return RedissonPromise.newSucceededFuture(true);}return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,//执行命令"get semaphore",获取到当前值"local value = redis.call('get', KEYS[1]); "+//如果semaphore的当前值不是false,且大于客户端线程申请获取锁的数量"if (value ~= false and tonumber(value) >= tonumber(ARGV[1])) then " +//执行"decrby semaphore 1",将信号量允许获取锁的总数量递减1"local val = redis.call('decrby', KEYS[1], ARGV[1]); " +"return 1; " +"end; " +//如果semaphore的值变为0,那么客户端就无法获取锁了,此时返回false"return 0;",Collections.<Object>singletonList(getRawName()),permits//ARGV[1]默认是1);}...
}public class CommandAsyncService implements CommandAsyncExecutor {...@Overridepublic <V> V getNow(CompletableFuture<V> future) {try {return future.getNow(null);} catch (Exception e) {return null;}}...
}public class RedissonLockEntry implements PubSubEntry<RedissonLockEntry> {private final Semaphore latch;...public RedissonLockEntry(CompletableFuture<RedissonLockEntry> promise) {super();this.latch = new Semaphore(0);this.promise = promise;}public Semaphore getLatch() {return latch;}...
}

执行命令"get semaphore",获取到semaphore的当前值。如果semaphore的当前值不是false,且大于客户端线程申请获取锁的数量。那么就执行"decrby semaphore 1",将信号量允许获取锁的总数量递减1。

如果semaphore的值变为0,那么客户端就无法获取锁了,此时tryAcquire()方法返回false。表示获取semaphore的锁失败了,于是当前客户端线程便会通过本地JDK的Semaphore进行阻塞。

当客户端后续收到一个订阅事件把本地JDK的Semaphore进行释放后,便会唤醒阻塞线程继续while循环。在while循环中,会不断尝试获取这个semaphore的锁,如此循环往复,直到成功获取。

(4)客户端释放Semaphore的锁

public class RedissonSemaphore extends RedissonExpirable implements RSemaphore {...@Overridepublic void release() {release(1);}@Overridepublic void release(int permits) {get(releaseAsync(permits));}@Overridepublic RFuture<Void> releaseAsync(int permits) {if (permits < 0) {throw new IllegalArgumentException("Permits amount can't be negative");}if (permits == 0) {return RedissonPromise.newSucceededFuture(null);}RFuture<Void> future = commandExecutor.evalWriteAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_VOID,//执行命令"incrby semaphore 1""local value = redis.call('incrby', KEYS[1], ARGV[1]); " +"redis.call('publish', KEYS[2], value); ",Arrays.asList(getRawName(), getChannelName()),permits);if (log.isDebugEnabled()) {future.onComplete((o, e) -> {if (e == null) {log.debug("released, permits: {}, name: {}", permits, getName());}});}return future;}...
}//订阅semaphore不为0的事件,semaphore不为0时会触发执行这里的监听回调
public class SemaphorePubSub extends PublishSubscribe<RedissonLockEntry> {public SemaphorePubSub(PublishSubscribeService service) {super(service);}@Overrideprotected RedissonLockEntry createEntry(CompletableFuture<RedissonLockEntry> newPromise) {return new RedissonLockEntry(newPromise);}@Overrideprotected void onMessage(RedissonLockEntry value, Long message) {Runnable runnableToExecute = value.getListeners().poll();if (runnableToExecute != null) {runnableToExecute.run();}//将客户端本地JDK的Semaphore进行释放value.getLatch().release(Math.min(value.acquired(), message.intValue()));}
}//订阅锁被释放的事件,锁被释放为0时会触发执行这里的监听回调
public class LockPubSub extends PublishSubscribe<RedissonLockEntry> {public static final Long UNLOCK_MESSAGE = 0L;public static final Long READ_UNLOCK_MESSAGE = 1L;public LockPubSub(PublishSubscribeService service) {super(service);}  @Overrideprotected RedissonLockEntry createEntry(CompletableFuture<RedissonLockEntry> newPromise) {return new RedissonLockEntry(newPromise);}@Overrideprotected void onMessage(RedissonLockEntry value, Long message) {if (message.equals(UNLOCK_MESSAGE)) {Runnable runnableToExecute = value.getListeners().poll();if (runnableToExecute != null) {runnableToExecute.run();}value.getLatch().release();} else if (message.equals(READ_UNLOCK_MESSAGE)) {while (true) {Runnable runnableToExecute = value.getListeners().poll();if (runnableToExecute == null) {break;}runnableToExecute.run();}//将客户端本地JDK的Semaphore进行释放value.getLatch().release(value.getLatch().getQueueLength());}}
}

客户端释放Semaphore的锁时,会执行命令"incrby semaphore 1"。每当客户端释放掉permits个锁,就会将信号量的值累加permits,这样Semaphore信号量的值就不再是0了。然后通过publish命令发布一个事件,之后订阅了该事件的其他客户端都会对getLatch()返回的本地JDK的Semaphore进行加1。于是其他客户端正在被本地JDK的Semaphore进行阻塞的线程,就会被唤醒继续执行。此时其他客户端就可以尝试获取到这个信号量的锁,然后再次将这个Semaphore的值递减1。

4.Redisson的CountDownLatch简介

(1)Redisson的CountDownLatch原理图解

CountDownLatch的基本原理:要求必须有n个线程来进行countDown,才能让执行await的线程继续执行。如果没有达到指定数量的线程来countDown,会导致执行await的线程阻塞。

(2)Redisson的CountDownLatch使用演示

public class RedissonDemo {public static void main(String[] args) throws Exception {//连接3主3从的Redis CLusterConfig config = new Config();...//CountDownLatchfinal RedissonClient redisson = Redisson.create(config);RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");//1.设置可以countDown的数量为3latch.trySetCount(3);System.out.println(new Date() + ":线程[" + Thread.currentThread().getName() + "]设置了必须有3个线程执行countDown,进入等待中。。。");for (int i = 0; i < 3; i++) {new Thread(new Runnable() {public void run() {try {System.out.println(new Date() + ":线程[" + Thread.currentThread().getName() + "]在做一些操作,请耐心等待。。。。。。");Thread.sleep(3000);RCountDownLatch localLatch = redisson.getCountDownLatch("myCountDownLatch");localLatch.countDown();System.out.println(new Date() + ":线程[" + Thread.currentThread().getName() + "]执行countDown操作");} catch (Exception e) {e.printStackTrace();}}}).start();}latch.await();System.out.println(new Date() + ":线程[" + Thread.currentThread().getName() + "]收到通知,有3个线程都执行了countDown操作,可以继续往下执行");}
}

5.Redisson的CountDownLatch源码剖析

(1)CountDownLatch的初始

public class Redisson implements RedissonClient {//Redis的连接管理器,封装了一个Config实例protected final ConnectionManager connectionManager;//Redis的命令执行器,封装了一个ConnectionManager实例protected final CommandAsyncExecutor commandExecutor;...protected Redisson(Config config) {this.config = config;Config configCopy = new Config(config);//初始化Redis的连接管理器connectionManager = ConfigSupport.createConnectionManager(configCopy);...  //初始化Redis的命令执行器commandExecutor = new CommandSyncService(connectionManager, objectBuilder);...}@Overridepublic RCountDownLatch getCountDownLatch(String name) {return new RedissonCountDownLatch(commandExecutor, name);}...
}public class RedissonCountDownLatch extends RedissonObject implements RCountDownLatch {...private final CountDownLatchPubSub pubSub;private final String id;protected RedissonCountDownLatch(CommandAsyncExecutor commandExecutor, String name) {super(commandExecutor, name);this.id = commandExecutor.getConnectionManager().getId();this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getCountDownLatchPubSub();}...
}

(2)trySetCount()方法设置countDown的数量

trySetCount()方法的工作就是执行命令"set myCountDownLatch 3"。

public class RedissonCountDownLatch extends RedissonObject implements RCountDownLatch {...@Overridepublic boolean trySetCount(long count) {return get(trySetCountAsync(count));}@Overridepublic RFuture<Boolean> trySetCountAsync(long count) {return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if redis.call('exists', KEYS[1]) == 0 then " +"redis.call('set', KEYS[1], ARGV[2]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1 " +"else " +"return 0 " +"end",Arrays.asList(getRawName(), getChannelName()),CountDownLatchPubSub.NEW_COUNT_MESSAGE,count);}...
}

(3)awati()方法进行阻塞等待

public class RedissonCountDownLatch extends RedissonObject implements RCountDownLatch {...@Overridepublic void await() throws InterruptedException {if (getCount() == 0) {return;}CompletableFuture<RedissonCountDownLatchEntry> future = subscribe();try {commandExecutor.syncSubscriptionInterrupted(future);while (getCount() > 0) {// waiting for open state//获取countDown的数量还大于0,就先阻塞线程,然后再等待唤醒,执行while循环//其中getLatch()返回的是JDK的semaphore = "new Semaphore(0)" ==> (state - permits)commandExecutor.getNow(future).getLatch().await();}} finally {unsubscribe(commandExecutor.getNow(future));}}@Overridepublic long getCount() {return get(getCountAsync());}@Overridepublic RFuture<Long> getCountAsync() {//执行命令"get myCountDownLatch"return commandExecutor.writeAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.GET_LONG, getRawName());}...
}

在while循环中,首先会执行命令"get myCountDownLatch"去获取countDown值。如果该值不大于0,就退出循环不阻塞线程。如果该值大于0,则说明还没有指定数量的线程去执行countDown操作,于是就会先阻塞线程,然后再等待唤醒来继续循环。

(4)countDown()方法对countDown的数量递减

public class RedissonCountDownLatch extends RedissonObject implements RCountDownLatch {...@Overridepublic void countDown() {get(countDownAsync());}@Overridepublic RFuture<Void> countDownAsync() {return commandExecutor.evalWriteNoRetryAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"local v = redis.call('decr', KEYS[1]);" +"if v <= 0 then redis.call('del', KEYS[1]) end;" +"if v == 0 then redis.call('publish', KEYS[2], ARGV[1]) end;",Arrays.<Object>asList(getRawName(), getChannelName()),CountDownLatchPubSub.ZERO_COUNT_MESSAGE);}...
}

countDownAsync()方法会执行decr命令,将countDown的数量进行递减1。如果这个值已经小于等于0,就执行del命令删除掉该CoutDownLatch。如果是这个值为0,还会发布一条消息:

publish redisson_countdownlatch__channel__{anyCountDownLatch} 0

文章转载自:东阳马生架构

原文链接:分布式锁—6.Redisson的同步器组件 - 东阳马生架构 - 博客园

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/897784.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

国产编辑器EverEdit - 脚本(解锁文本编辑的无限可能)

1 脚本 1.1 应用场景 脚本是一种功能扩展代码&#xff0c;用于提供一些编辑器通用功能提供不了的功能&#xff0c;帮助用户在特定工作场景下提高工作效率&#xff0c;几乎所有主流的编辑器、IDE都支持脚本。   EverEdit的脚本支持js(语法与javascript类似)、VBScript两种编程…

服务器上的nginx因漏洞扫描需要升级

前言 最近客户联系说nginx存在安全漏洞 F5 Nginx 安全漏洞(CVE-2024-7347) F5Nginx是美国F5公司的一款轻量级Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器&#xff0c;在BSD-like协议下发行。F5 Nginx存在安全漏洞&#xff0c;该漏洞源于可能允许攻击者使用特制的…

ASP.NET CORE MVC EF框架

1.一个视图中的多个表单Form中的变量。 方式一&#xff1a;视图中跨Form变量不能用&#xff0c;得各自定义变量否则编译不能通过。变量名还不能相同。 或者方式二&#xff1a;在Form之外定义变量 {ViewData["Title"] "ExpenseForm"; } &#xfeff; {L…

【MySQL 中 `TINYINT` 类型与布尔值的关系】

MySQL 中 TINYINT 类型与布尔值的关系 在 MySQL 数据库中&#xff0c;BOOLEAN 类型并不存在&#xff0c;BOOLEAN 或 BOOL 都是 TINYINT(1) 的别名。通常&#xff0c;TINYINT(1) 类型用于存储布尔值。 1. TINYINT 类型介绍 TINYINT 是一个占用 1 字节的整数类型&#xff0c;取…

【Rust基础】Rust后端开发常用库

使用Rust有一段时间了&#xff0c;期间尝试过使用Rust做后端开发、命令行工具开发&#xff0c;以及做端侧模型部署&#xff0c;也尝试过交叉编译、FFI调用等&#xff0c;也算是基本入门了。在用Rust做后端接口开发时&#xff0c;常常会找不到一些合适库&#xff0c;而这些库在J…

[leetcode]位运算

一.AND &运算 注&#xff1a;两个操作数做&运算结果是不会变大的 二.OR |运算 注&#xff1a;两个操作数做|运算结果是不会变小的 三.XOR(异或) ^运算 注&#xff1a;结果可能变大也可能变小也可能不变&#xff0c;但是不会导致进位&#xff0c;比如两个四位的数字做…

常见FUZZ姿势与工具实战:从未知目录到备份文件漏洞挖掘

本文仅供学习交流使用&#xff0c;严禁用于非法用途。未经授权&#xff0c;禁止对任何网站或系统进行未授权的测试或攻击。因使用本文所述技术造成的任何后果&#xff0c;由使用者自行承担。请严格遵守《网络安全法》及相关法律法规&#xff01; 目录 本文仅供学习交流使用&am…

前置机跟服务器的关系

在复杂的IT系统架构中&#xff0c;前置机与服务器的协同配合是保障业务高效、安全运行的关键。两者的关系既非简单的上下级&#xff0c;也非独立个体&#xff0c;而是通过功能分层与职责分工&#xff0c;构建起一套既能应对高并发压力、又能抵御安全风险的弹性体系。 在当今复…

MySQL中有哪些索引

1&#xff0c;B-Tree索引&#xff1a;常见的索引类型 2&#xff0c;哈希索引&#xff1a;基于哈希表实现&#xff0c;只支持等值查询 &#xff0c;只有Memory存储引擎和NDB Cluster存储引擎显示支持哈希索引 3&#xff0c;全文索引&#xff1a;可在字符列上创建&#xff08;T…

Python爬虫---中国大学MOOC爬取数据(文中有数据集)

1、内容简介 本文为大二在校学生所做&#xff0c;内容为爬取中国大学Mooc网站的课程分类数据、课程数据、评论数据。数据集大佬们需要拿走。主要是希望大佬们能指正代码问题。 2、数据集 课程评论数据集&#xff0c;343525条&#xff08;包括评论id、评论时间、发送评论用户…

Tomcat 安装

一、Tomcat 下载 官网&#xff1a;Apache Tomcat - Welcome! 1.1.下载安装包 下载安装包&#xff1a; wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.102/bin/apache-tomcat-9.0.102.tar.gz 安装 javajdk。 yum install java-1.8.0-openjdk.x86_64 -y /etc/altern…

MC34063数据手册解读:功能、应用与设计指南

MC34063A/MC33063A 系列是摩托罗拉&#xff08;现 NXP&#xff09;推出的高集成度 DC-DC 转换器控制电路&#xff0c;适用于降压、升压和反相应用。本文将基于官方数据手册&#xff0c;对其核心功能、关键参数、典型应用及设计要点进行详细解读。 一、核心功能与特性 集成度高…

基于SpringBoot实现旅游酒店平台功能十一

一、前言介绍&#xff1a; 1.1 项目摘要 随着社会的快速发展和人民生活水平的不断提高&#xff0c;旅游已经成为人们休闲娱乐的重要方式之一。人们越来越注重生活的品质和精神文化的追求&#xff0c;旅游需求呈现出爆发式增长。这种增长不仅体现在旅游人数的增加上&#xff0…

Linux入门 全面整理终端 Bash、Vim 基础命令速记

Linux入门 2025 超详细全面整理 Bash、Vim 基础命令速记 刚面对高级感满满的 终端窗口是不是有点懵&#xff1f;于是乎&#xff0c;这份手册就是为你准备的高效学习指南&#xff01;我把那些让人头大的系统设置、记不住的命令都整理成了对你更友好的格式&#xff0c;让你快速学…

基于deepseek的图像生成系统

目录 问题 核心思路 pollinations 提示词 基于deepseek的图像生成系统 项目说明 详细说明 1. 注册流程 2. 登录流程 3. 图片生成流程 4. 图片下载流程 项目结构 代码实现 1. 配置文件 config.py 2. 数据库模型 models.py 3. 解决循环引用 exts.py 4. 登录和…

mac安装mysql之后报错zsh: command not found: mysql !

在Mac上安装MySQL后&#xff0c;如果终端中找不到mysql命令&#xff0c;通常是 因为MySQL的命令行工具&#xff08;如mysql客户端&#xff09;没有被正确地添加到你的环境变量中。 检查 MySQL 是否已安装 ps -ef|grep mysql查看到路径在 /usr/local/mysql/bin 查看 .bash_pro…

骨质健康护理笔记

1. 阅读资料 《骨质疏松症不是“老年病”&#xff01;除了补钙、晒太阳&#xff0c;专家还推荐… —— 健康湖北》

CSS3 用户界面设计指南

CSS3 用户界面设计指南 引言 随着互联网的快速发展,用户界面设计已经成为网站和应用程序吸引和留住用户的关键因素之一。CSS3,作为Web开发中的核心技术之一,提供了丰富的工具和特性来改善用户界面。本文将深入探讨CSS3在用户界面设计中的应用,包括基本概念、常用技巧以及…

Mybatis3 调用存储过程

1. 数据库MySQL&#xff0c;user表 CREATE TABLE user (USER_ID int NOT NULL AUTO_INCREMENT,USER_NAME varchar(100) NOT NULL COMMENT 用户姓名,AGE int NOT NULL COMMENT 年龄,CREATED_TIME datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,CREATED_BY varchar(100) NOT NUL…

Uniapp组件 Textarea 字数统计和限制

Uniapp Textarea 字数统计和限制 在 Uniapp 中&#xff0c;可以通过监听 textarea 的 input 事件来实现字数统计功能。以下是一个简单的示例&#xff0c;展示如何在 textarea 的右下角显示输入的字符数。 示例代码 首先&#xff0c;在模板中定义一个 textarea 元素&#xff…