Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

谷歌新出了个对标claude code的命令行工具,叫gemini-cli,还是开源的,所以赶紧来体验下,就是前提是有科学上网,要去google的ai studio申请gemini的api key
然后在有科学网络的情况下用node 18及以上的版本安装

1
npm install -g @google/gemini-cli

然后在环境变量里设置gemini key

1
export GEMINI_API_KEY="xxxxxxxx"

接着运行gemini就可以啦
这里我是刚好有个需求,因为娃最近有点不太爱喝奶,经常剩下,想要记录统计下,看看每天的总的奶量
那我就建了个空的目录,并在这个目录下运行了 gemini ,然后输入了我的需求,
原始的诉求是这样
我想要写个网页版小程序,可以按时间记录宝宝每次的奶量,包括泡了多少,剩了多少,能按天统计每天的奶量,以及查看趋势
然后gemini就开始运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
✦ 好的,这个需求很明确,我们可以创建一个名为“奶量备忘” (Milk Memo) 的网页应用来实现它。


这是一个很好的项目,完全可以通过前端技术实现,将数据存储在浏览器中,无需复杂的后端。

这是我的计划:


应用名称:奶量备忘 (Milk Memo)


核心功能:
1. 记录喂奶量:一个简洁的表单,用于输入“冲泡量”和“剩余量”。提交后,应用会自动计算“实际饮用量”并记录当前时间。
2. 每日记录列表:清晰地展示当天的所有喂奶记录,包括时间、冲泡量、剩余量和实际饮用量。
3. 今日统计:在页面顶部或底部显眼地展示当天累计的总饮用量。
4. 历史趋势:一个图表页面,用柱状图或折线图展示最近7天或30天的每日总饮用量,帮助您直观地了解宝宝的饮奶趋势。


技术选型:
* 前端框架: React - 用于构建用户界面,组件化的思想很适合这个应用。
* 样式与布局: Bootstrap - 一个流行的CSS框架,可以快速构建出简洁、美观且移动端优先的响应式布局。
* 图表: Chart.js - 一个强大的图表库,用于实现“历史趋势”功能。
* 数据存储: 浏览器本地存储 (Local Storage) - 无需后端和数据库,所有数据将安全地存储在您自己的浏览器里,简单快捷。


用户体验与设计:
我将采用现代化、简洁的设计风格,确保在手机浏览器上也能获得良好的操作体验。界面会以卡片的形式展示数据,按钮和输入框都会清晰易用。


这个计划听起来如何?如果同意,我将开始着手创建这个应用。

然后我输入了同意他就开始帮我实现了
它会请求我的同意来运行比如npm命令,安装依赖等等
然后就自己运行了npm start 来演示运行这个应用

不算非常有设计感,但是基本的功能是已经实现了
接着我又让它帮我增加了时间选项,以及趋势图查看功能,
接着它就又生成了一版

1
2
3
4
5
6
7
8
9
✦ 功能已经更新完毕。

现在应用具备了以下功能:


* 自定义时间:您可以在添加记录时,精确选择到分钟。
* 视图切换:通过顶部的按钮,可以在“记录列表”和“历史趋势”图表之间自由切换。
* 趋势图表:在“历史趋势”视图中,您可以看到一个折线图,展示了每天的总饮奶量。
* 数据排序:所有记录现在会按时间从近到远排序,方便查看最近的条目。


感觉完成了所需的95%的功能,除了在设计上还能再美观些,以及增加一些服务端存储数据的功能,基本是个比较完善的,我自己都完全没介入,的确ai的发展真的一日千里,另外我还是比较看好谷歌的,没有什么大新闻,就一直努力追赶,现在基本已经是第一梯队了,很好很强大

之前介绍过wsl2的ssh连接问题,但是最近又碰到无法连接的情况,通过一顿查资料
发现了个比较特别的方法
首先可以通过powershell命令查看目前的端口开放情况

1
Get-NetFirewallRule | Where-Object {$_.LocalPort -eq 22 -or $_.DisplayName -like "*SSH*"} | Select-Object DisplayName, Enabled, Action, Direction

这个是查看的22端口或者名称带有SSH的
还可以直接看22端口的入站规则

1
Get-NetFirewallRule -Direction Inbound | Where-Object {$_.LocalPort -eq 22} | Select-Object DisplayName, Enabled, Action

确认下是否已有对应规则,没有的话可以通过下面的命令来添加
方法1:允许所有网络的ssh访问

1
New-NetFirewallRule -DisplayName "WSL2 SSH Inbound" -Direction Inbound -Protocol TCP -LocalPort 22 -Action Allow

当然这样不太安全
所以我们最好是指定下来源ip
方法二:只允许局域网ip来访问

1
New-NetFirewallRule -DisplayName "WSL2 SSH LAN" -Direction Inbound -Protocol TCP -LocalPort 22 -Action Allow -RemoteAddress 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12

后面的网段可以根据所需限定
然后可以在所需机器进行访问验证

1
ssh -v user@192.168.xxx.xxx

可以访问了的话说明我们这个就已经放行了,通过这个我们也能发现其实wsl2的这种方式一方面是让linux更加完整,但是对于所处的windows宿主机,想进行一些连通访问就会变得更加麻烦,也算是种取舍

发现一个比较奇怪的问题,在看一个依赖的源码的时候没法下载sources,idea点击的时候报”cannot download sources”
可能有点强迫症,看着缩略过的代码比较不舒服,就查了下这个问题的解决方法

第一种maven命令

首先第一种是直接使用maven命令
在项目目录下执行

1
mvn dependency:resolve -Dclassifier=sources

命令,这样就会去下载这些依赖包的sources源码,等待命令构建完成就可以了
如果idea自动重建索引了就直接生效了,如果没有则需要choose sources

第二种idea设置

还有一种,找到idea的设置中的build工具,maven,import设置,勾选自动下载sources

可以勾选sources,就会在下载依赖包的时候就自动下载sources
点击idea的maven刷新按钮就会在下载依赖包的时候同代下载sources源码
这些前提是本身打包的时候是带上sources源码的,没有用一些混淆加密手段过

我们在用一些设计模式的时候,特别是在java跟spring生态中,有很多基于接口,会有各种不同的实现,此时如果想让实现了同一个接口的一组处理器(bean)能够按顺序处理,会有比较多种方法,这里简单介绍两种实现方式,
我们先定义一个接口

1
2
3
public interface Processor {
void process();
}

然后有两个实现
第一个实现是

1
2
3
4
5
6
7
public class FirstProcessor implements Processor{

@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}
}

第二个实现是

1
2
3
4
5
6
7
public class SecondProcessor implements Processor{

@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}
}

主要是为了示意,所以就把类名打了出来
我们通过一个http接口来调用下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class OrderController {

@Autowired
private List<Processor> processors;

@RequestMapping(value = "/order", method = RequestMethod.GET)
@ResponseBody
public void test() {
for (Processor processor : processors) {
processor.process();
}
}
}

这里通过autowired注解进行注入,此时我们可以运行起来看下执行顺序
看到控制台输出的确是有序的

1
2
this is FirstProcessor
this is SecondProcessor

这个并不是spring这么智能知道我们的想法,只是刚好按字母顺序F在前
比如我们再加两个处理器
第三个

1
2
3
4
5
6
7
8
@Service
public class ThirdProcessor implements Processor{

@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}
}

和第四个

1
2
3
4
5
6
7
@Service
public class FourthProcessor implements Processor{
@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}
}

然后我们请求下,就发现是

1
2
3
4
this is FirstProcessor
this is FourthProcessor
this is SecondProcessor
this is ThirdProcessor

没有按照我们想要的顺序执行
那么我们可以怎么做呢

方法一 使用order注解

通过spring的org.springframework.core.annotation.Order 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.springframework.core.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
int value() default Integer.MAX_VALUE;
}

通过value值来指定顺序,值越小,顺序越靠前
比如我们这

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@Order(value = 1)
public class FirstProcessor implements Processor{
@Service
@Order(value = 2)
public class SecondProcessor implements Processor{
@Service
@Order(value = 3)
public class ThirdProcessor implements Processor{
@Service
@Order(value = 4)
public class FourthProcessor implements Processor{

运行验证下的确是按照我们想要的顺序执行了

1
2
3
4
this is FirstProcessor
this is SecondProcessor
this is ThirdProcessor
this is FourthProcessor

方法二 实现 Ordered 接口

org.springframework.core.Ordered 接口,需要实现 getOrder 方法

1
2
3
4
5
6
public interface Ordered {
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

int getOrder();
}

我们就在处理器里实现这个接口方法

1
2
3
4
5
6
7
8
9
10
11
12
public class FirstProcessor implements Processor, Ordered {

@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}

@Override
public int getOrder() {
return 1;
}
}

运行下同样可以实现这个效果

1
2
3
4
this is FirstProcessor
this is SecondProcessor
this is ThirdProcessor
this is FourthProcessor

很多的业务场景我们会用到锁,包括各种炫酷的分布式锁,但是其实很多情况由于db的可靠性是相对比较高的,所以也可以在适当的情况下使用db来作为锁
这里就介绍下比较常用了 select for update 锁

什么是 SELECT FOR UPDATE

SELECT FOR UPDATE 是 MySQL 中一种特殊的查询语句,它在执行查询的同时对选中的行加上排他锁(Exclusive Lock),确保在当前事务结束之前,其他事务无法修改这些数据。这种机制主要用于解决并发环境下的数据一致性问题。

基本语法

1
SELECT column1, column2, ... FROM table_name WHERE condition FOR UPDATE;

工作原理

当执行 SELECT FOR UPDATE 时,MySQL 会:

  1. 加排他锁:对查询结果中的每一行数据加上排他锁(X锁)
  2. 阻塞其他事务:其他事务如果要修改被锁定的行,必须等待当前事务结束
  3. 事务结束释放:当前事务提交(COMMIT)或回滚(ROLLBACK)时,锁会自动释放

使用场景

1. 防止超卖问题

电商系统中最常见的应用场景:

1
2
3
4
5
6
7
8
9
10
11
12
-- 开始事务
START TRANSACTION;

-- 查询并锁定商品库存
SELECT stock FROM products WHERE id = 1001 FOR UPDATE;

-- 假设查询结果显示库存为 10
-- 用户购买 3 件商品
UPDATE products SET stock = stock - 3 WHERE id = 1001;

-- 提交事务
COMMIT;

这里也可以直接使用start; 开启事务,

2. 生成唯一序列号

确保序列号的唯一性:

1
2
3
4
5
6
7
8
9
10
START TRANSACTION;

-- 获取当前最大序列号并锁定
SELECT max_seq FROM sequence_table WHERE table_name = 'orders' FOR UPDATE;

-- 更新序列号
UPDATE sequence_table SET max_seq = max_seq + 1 WHERE table_name = 'orders';

-- 提交事务
COMMIT;

3. 账户余额操作

银行转账等涉及账户余额的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
START TRANSACTION;

-- 锁定转出账户
SELECT balance FROM accounts WHERE account_id = 'A001' FOR UPDATE;

-- 锁定转入账户
SELECT balance FROM accounts WHERE account_id = 'B001' FOR UPDATE;

-- 执行转账操作
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 'A001';
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 'B001';

COMMIT;

锁的范围和类型

1. 行级锁 vs 表级锁

  • InnoDB 存储引擎:使用行级锁,只锁定符合条件的行
  • MyISAM 存储引擎:使用表级锁,锁定整个表

2. 索引对锁范围的影响

1
2
3
4
5
6
7
8
-- 情况1:使用主键或唯一索引(精确锁定)
SELECT * FROM users WHERE id = 100 FOR UPDATE; -- 只锁定 id=100 的行

-- 情况2:使用普通索引(可能锁定多行)
SELECT * FROM users WHERE age = 25 FOR UPDATE; -- 锁定所有 age=25 的行

-- 情况3:无索引条件(锁定所有行)
SELECT * FROM users WHERE name = 'John' FOR UPDATE; -- 可能锁定整个表

死锁问题与预防

死锁产生的原因

1
2
3
4
5
6
7
8
9
-- 事务 A
START TRANSACTION;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE; -- 等待事务B释放

-- 事务 B(同时执行)
START TRANSACTION;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE; -- 等待事务A释放

这样就会造成死锁等待,注意一点死锁不一定是两个事务相互等待,也可以是循环等待,要达到的是竞态等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 事务 A
START TRANSACTION;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE; -- 等待事务B释放

-- 事务 B(同时执行)
START TRANSACTION;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE;
SELECT * FROM table1 WHERE id = 3 FOR UPDATE; -- 等待事务C释放

-- 事务 C(同时执行)
START TRANSACTION;
SELECT * FROM table2 WHERE id = 3 FOR UPDATE;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE; -- 等待事务A释放

这样也会造成死锁,所以不要把死锁就单纯理解为两个线程互相等待

预防死锁的策略

  1. 统一加锁顺序:所有事务按相同顺序获取锁
  2. 减少锁持有时间:尽快提交或回滚事务
  3. 使用较低的隔离级别:在业务允许的情况下
  4. 合理设计索引:避免锁定过多不必要的行

性能优化建议

1. 合理使用索引

1
2
3
4
5
-- 优化前:全表扫描,锁定大量行
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;

-- 优化后:为 status 字段添加索引
ALTER TABLE orders ADD INDEX idx_status (status);

2. 缩小锁定范围

1
2
3
4
5
-- 避免:锁定所有字段
SELECT * FROM products WHERE category = 'electronics' FOR UPDATE;

-- 推荐:只选择必要字段
SELECT id, stock FROM products WHERE category = 'electronics' FOR UPDATE;

3. 合理的事务边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 不推荐:事务过长
START TRANSACTION;
SELECT * FROM table1 FOR UPDATE;
-- 大量业务逻辑处理
-- 网络IO操作
UPDATE table1 SET ...;
COMMIT;

-- 推荐:缩短事务时间
-- 先处理业务逻辑
START TRANSACTION;
SELECT * FROM table1 FOR UPDATE;
UPDATE table1 SET ...;
COMMIT;

注意事项和限制

1. 必须在事务中使用

1
2
3
4
5
6
7
8
-- 错误:不在事务中,锁立即释放
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 正确:在事务中使用
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 其他操作
COMMIT;

2. 自动提交模式的影响

1
2
3
4
5
6
7
-- 检查自动提交状态
SHOW VARIABLES LIKE 'autocommit';

-- 如果开启了自动提交,需要显式开启事务
SET autocommit = 0; -- 关闭自动提交
-- 或者
START TRANSACTION; -- 显式开启事务

3. 锁等待超时

1
2
-- 设置锁等待超时时间(秒)
SET innodb_lock_wait_timeout = 60;

替代方案

1. 乐观锁

1
2
3
4
-- 使用版本号实现乐观锁
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1001 AND version = @old_version;

2. 原子性操作

1
2
3
4
-- 直接使用原子性的 UPDATE
UPDATE products
SET stock = stock - 1
WHERE id = 1001 AND stock >= 1;

总结

SELECT FOR UPDATE 是 MySQL 中解决并发问题的重要工具,但使用时需要注意:

  • 合理设计事务边界,避免长时间持有锁
  • 正确使用索引,减少锁定范围
  • 统一加锁顺序,预防死锁
  • 根据业务场景选择合适的并发控制策略

正确使用 SELECT FOR UPDATE 可以有效保证数据一致性,但也要权衡其对系统性能的影响,在实际应用中需要根据具体业务场景做出合理的选择。

0%