静かな駅
type: channel
link: https://yellowswimming.cn/
description: 一个记录学习经历的小博主
items: 乘法快速幂
items: pta实验五
items: pta实验四
items: pta实验三
items: pta实验二
items: pta实验一
items: Lua脚本解决高并发与库存超卖
items: 权限操作：RBAC+ACL搭配
items: 微服务架构-Outbox发件箱模式
items: 设计模式原则分析

乘法快速幂
type: item
date: 2025-12-06T21:15:00+08:00
description: 乘法快速幂 如果需要求x^n，那么最简单的办法就是对x进行n次相乘，此时时间复杂度为O（n），而乘法快速幂可以是O（logn）的复杂度 举例 假设需要求10^75,首先对75转为二进制，为 对于该例子，x=10，设置每次x = x*x 设置一个ans变量，每次有有选择地乘以x，具体选择方式如下： x=10^1，发现1对应的二进制那一位为1，则ans = 1*x= 1*10^1 然后x=x*x，即x=10^2，发现对应的二进制那一位为1，则ans = 1*10^1*10^2 然后x=x*x，即x=10^4，发现对应的二进制那一位为0，则ans = 1*10^1*10^2，ans不变 以此类推，总结下来就是先把n变成二进制，然后x每次都与自己相乘，遇到二进制对应的位为1就乘进ans里面，否则ans不动 对应的java代码如下： public static long power(long x, long n,long mod) { long res = 1; while (n > 0) { if ((n & 1)!= 0) res = (res * x) % mod; n = n >> 1; x = (x * x) % mod; } return res; }

pta实验五
type: item
date: 2025-11-14T22:35:38+08:00
description: 实验五 一. 7-1 部分排序 对于一组数据，我们可以只对原先处在中间位置的那些元素进行排序。 输入格式: 在一行内输入n r a1 a2 ... an 其中，不大于200的正整数n表示该组数据的个数；不大于200的非负整数r表示该组数据两端各自留有r个数不参与排序，若r+r>=n，则该组数据无需排序。 整数a1 a2 ... an是该组的n个数据，且都在8位以内。 输出格式: 排序之后的序列，元素之间用一个空格间隔，最后一个元素之后不加空格。 输入样例: 5 1 6 5 4 3 2 输出样例: 6 3 4 5 2 参考答案： #include<iostream> #include<vector> using namespace std; int main(){ int n,r; cin>>n>>r; vector<int>ivec; int num; for(int i=0;i<n;i++){ cin>>num; ivec.push_back(num); } if(r+r<n){ sort(ivec.begin()+r,ivec.begin()+n-r); } bool isHead = true; for(auto &c:ivec){ if(isHead){ cout<<c; isHead = !isHead; } else{ cout<<" "<<c; } } cout<<endl; return 0; } 二. 7-2 组最大数 设有n个正整数，将他们连接成一排，组成一个最大的多位整数。 如:n=3时，3个整数13,312,343连成的最大整数为34331213。 如:n=4时，4个整数7,13,4,246连接成的最大整数为7424613。 输入格式: 有多组测试样例，每组测试样例包含两行，第一行为一个整数N（N<=100），第二行包含N个数(每个数不超过1000，空格分开)。 输出格式: 每组数据输出一个表示最大的整数。 输入样例: 2 12 123 4 7 13 4 246 输出样例: 12312 7424613 参考答案： #include<iostream> #include<vector> using namespace std; bool compare(int a,int b){ string sa = to_string(a); string sb = to_string(b); return sa + sb > sb + sa; } int main(){ int n; while(cin>>n){ vector<int>ivec; int num; for(int i=0;i<n;i++){ cin>>num; ivec.push_back(num); } sort(ivec.begin(),ivec.end(),compare); for(auto &s:ivec){ cout<<s; } ivec.clear(); cout<<endl; } return 0; } 三. 7-3 办事大厅排队 在郑州大学综合办事大厅，每天陆陆续续有很多人来排队办事。现在你能否写程序帮助老师时刻了解当前办理业务的情况。 请同学们学习C++ STL中 list相关内容后，编程实践。 输入格式: 第一行一个数字N，表示排队信息或者查询信息条目的数量。 以下N行，每行的内容有以下3种情况 (1) in name 表示名字为name的人员新来到办事大厅，排在队伍的最后。（in和name间存在一个空格，name是名字对应字符串，长度不超过10）。 (2) out 表示当前排在最前面的人已经办理完业务，离开了。 (3) q 表示一次查询，请输出当前正在办理业务的人，也就是队伍的第1个人。如果当前无人办理业务，则输出“NULL”，不包括引号。 输出格式: 请根据以上信息，每次遇到查询时，对应一行输出。如果这时队伍有人，则输出第一个人的姓名，否则输出NULL。 输入样例: 5 in A out q in B q 输出样例: 在这里给出相应的输出。例如： NULL B 参考答案： #include<iostream> #include<vector> #include<deque> using namespace std; int main(){ int n; cin>>n; deque<string>person; string op,name; while(n-->0){ cin>>op; if(op=="q"){ if(person.size()>0){ cout<<person.front()<<endl; } else{ cout<<"NULL"<<endl; } } else if(op=="out"){ person…

pta实验四
type: item
date: 2025-11-14T22:32:00+08:00
description: 实验四 一. 7-1 简单求阶乘问题 本题要求编写程序，计算N的阶乘。 输入格式: 输入在一行中给出一个不超过12的正整数N。 输出格式: 在一行中输出阶乘的值。 输入样例: 4 输出样例: 24 参考答案： #include<iostream> using namespace std; int f(int n){ if(n==1||n==0){ return 1; } return n*f(n-1); } int main(){ int n; cin>>n; cout<<f(n); return 0; } 二. 7-2 出生年 以上是新浪微博中一奇葩贴：“我出生于1988年，直到25岁才遇到4个数字都不相同的年份。”也就是说，直到2013年才达到“4个数字都不相同”的要求。本题请你根据要求，自动填充“我出生于y年，直到x岁才遇到n个数字都不相同的年份”这句话。 输入格式： 输入在一行中给出出生年份y和目标年份中不同数字的个数n，其中y在[1, 3000]之间，n可以是2、或3、或4。注意不足4位的年份要在前面补零，例如公元1年被认为是0001年，有2个不同的数字0和1。 输出格式： 根据输入，输出x和能达到要求的年份。数字间以1个空格分隔，行首尾不得有多余空格。年份要按4位输出。注意：所谓“n个数字都不相同”是指不同的数字正好是n个。如“2013”被视为满足“4位数字都不同”的条件，但不被视为满足2位或3位数字不同的条件。 输入样例1： 1988 4 输出样例1： 25 2013 输入样例2： 1 2 输出样例2： 0 0001 参考答案： #include<iostream> #include<set> using namespace std; bool isValid(int Y,int num){ string s = to_string(Y); set<char>mySet; if(s.size()<4){ for(char c:s){ mySet.insert(c); } mySet.insert('0'); return mySet.size()==num; } for(char c:s){ mySet.insert(c); } return mySet.size()==num; } string toString(int num){ string s = "0000"; string origin = to_string(num); int i = origin.size()-1; int j = s.size()-1; while(i>=0){ s[j--] = origin[i--]; } return s; } int main(){ int Y,num; cin>>Y>>num; int index = 0; while(true){ if(isValid(Y+index,num)){ cout<<index<<" "<<toString(Y+index); return 0; } index++; } return 0; } 三. 7-3 验证“哥德巴赫猜想” 数学领域著名的“哥德巴赫猜想”的大致意思是：任何一个大于2的偶数总能表示为两个素数之和。比如：24=5+19，其中5和19都是素数。本实验的任务是设计一个程序，验证20亿以内的偶数都可以分解成两个素数之和。 输入格式： 输入在一行中给出一个(2, 2 000 000 000]范围内的偶数N。 输出格式： 在一行中按照格式“N = p + q”输出N的素数分解，其中p ≤ q均为素数。又因为这样的分解不唯一（例如24还可以分解为7+17），要求必须输出所有解中p最小的解。 输入样例： 24 输出样例： 24 = 5 + 19 参考答案： #include<iostream> #include<cmath> using namespace std; bool isValid(long long n){ if(n<2)return false; for(long long i=2;i*i<=n;i++){ if(n%i==0)return false; } return true; } int main(){ long long n; cin>>n; for(long long i=2;i<=n/2;i++){ if(isValid(i)&&isValid(n-i)){ cout<<n<<" = "<<i<<" + "<<n-i; return 0; } } return 0; } 四. 7-4 谷歌的招聘 2004 年 7 月，谷歌在硅谷的 101 号公路边竖立了一块巨大的广告牌（如下图）用于招聘。内容超级简单，就是一个以 .com 结尾的网址，而…

pta实验三
type: item
date: 2025-11-14T22:26:00+08:00
description: 实验三 一. 7-1 求最大值及其下标 本题要求编写程序，找出给定的n个数中的最大值及其对应的最小下标（下标从0开始）。 输入格式: 输入在第一行中给出一个正整数n（1<n≤10）。第二行输入n个整数，用空格分开。 输出格式: 在一行中输出最大值及最大值的最小下标，中间用一个空格分开。 输入样例: 6 2 8 10 1 9 10 输出样例: 10 2 参考答案： #include<iostream> #include<climits> using namespace std; int main(){ int n; int index = 0; cin>>n; int max = INT_MIN; for(int i=0;i<n;i++){ int num; cin>>num; if(num>max){ index = i; max = num; } } cout<<max<<" "<<index; return 0; } 二. 7-2 统计字符出现次数 本题要求编写程序，统计并输出某给定字符在给定字符串中出现的次数。 输入格式： 输入第一行给出一个以回车结束的字符串（少于80个字符）；第二行输入一个字符。 输出格式： 在一行中输出给定字符在给定字符串中出现的次数。 输入样例： programming is More fun! m 输出样例： 2 参考答案： #include<stdio.h> #include<string.h> int main() { int i,len,count=0; char str[80]; char ch; gets(str);//输入str ch=getchar();//输入ch len=strlen(str);//str字符串的长度 for(i=0;i<len;i++) { if(str[i]==ch) count++; } printf("%d",count); return 0; } 三. 7-3 出租 下面是新浪微博上曾经很火的一张图： 一时间网上一片求救声，急问这个怎么破。其实这段代码很简单，index数组就是arr数组的下标，index[0]=2 对应 arr[2]=1，index[1]=0 对应 arr[0]=8，index[2]=3 对应 arr[3]=0，以此类推…… 很容易得到电话号码是18013820100。 本题要求你编写一个程序，为任何一个电话号码生成这段代码 —— 事实上，只要生成最前面两行就可以了，后面内容是不变的。 输入格式： 输入在一行中给出一个由11位数字组成的手机号码。 输出格式： 为输入的号码生成代码的前两行，其中arr中的数字必须按递减顺序给出。 输入样例： 18013820100 输出样例： int[] arr = new int[]{8,3,2,1,0}; int[] index = new int[]{3,0,4,3,1,0,2,4,3,4,4}; 参考答案： #include<iostream> #include<climits> #include<vector> #include<set> using namespace std; void print(vector<int>vec){ std::for_each(vec.begin(), vec.end(), [](int val) { std::cout << val << " "; }); cout<<endl; } int main(){ string tel; cin>>tel; set<int,greater<int>>mySet; for(char c:tel){ mySet.insert(c-'0'); } vector<int>index(10,0); int pos = 0; for(int val:mySet){ index[val] = pos; pos++; } vector<int>ivec; for(char c:tel){ ivec.push_back(index[c-'0']); } cout<<"int[] arr = new int[]{"; bool isHead = true; for(auto i:mySet){ if(isHead){ cout<<i; isHead = !isHead; } else{ cout<<","<<i; } } cout<<"};"<<endl; cout<<"int[] index = new int[]{"; isHead = true; for(auto i:ivec){ if(isHead){ cout<<i; isHead = !isHead; } else{ cout<<","<<i; } } cout<<"}…

pta实验二
type: item
date: 2025-11-14T22:23:01+08:00
description: 实验二 一. 7-1 奇偶分家 给定N个正整数，请统计奇数和偶数各有多少个？ 输入格式： 输入第一行给出一个正整N（≤1000）；第2行给出N个非负整数，以空格分隔。 输出格式： 在一行中先后输出奇数的个数、偶数的个数。中间以1个空格分隔。 输入样例： 9 88 74 101 26 15 0 34 22 77 输出样例： 3 6 参考答案： #include<iostream> using namespace std; int main(){ int n; int odd = 0; //奇数 int even = 0; //偶数 cin>>n; for(int i=0;i<n;i++){ int num; cin>>num; if(num%2==0){ even++; } else{ odd++; } } cout<<odd<<" "<<even; return 0; } 二. 7-1 水仙花数 水仙花数是指一个N位正整数（N≥3），它的每个位上的数字的N次幂之和等于它本身。例如：153=13+53+33。 本题要求编写程序,计算所有N位水仙花数。 输入格式: 输入在一行中给出一个正整数N（3≤N≤7）。 输出格式: 按递增顺序输出所有N位水仙花数，每个数字占一行。 输入样例: 3 输出样例: 153 370 371 407 参考答案: #include<iostream> #include<cmath> using namespace std; bool isValid(int num,int N){ int sum = 0; int tmp = num; while(tmp!=0){ int left = tmp%10; sum+=pow(left,N); tmp/=10; } return sum == num; } int main(){ int N; cin>>N; for(int i=pow(10,N-1);i<pow(10,N);i++){ if(isValid(i,N)){ cout<<i<<endl; } } return 0; } 三. 7-2 猜数字游戏 猜数字游戏是令游戏机随机产生一个100以内的正整数，用户输入一个数对其进行猜测，需要你编写程序自动对其与随机产生的被猜数进行比较，并提示大了（“Too big”），还是小了（“Too small”），相等表示猜到了。如果猜到，则结束程序。程序还要求统计猜的次数，如果1次猜出该数，提示“Bingo!”；如果3次以内猜到该数，则提示“Lucky You!”；如果超过3次但是在N（>3）次以内（包括第N次）猜到该数，则提示“Good Guess!”；如果超过N次都没有猜到，则提示“Game Over”，并结束程序。如果在到达N次之前，用户输入了一个负数，也输出“Game Over”，并结束程序。 输入格式: 输入第一行中给出两个不超过100的正整数，分别是游戏机产生的随机数、以及猜测的最大次数N。最后每行给出一个用户的输入，直到出现负数为止。 输出格式: 在一行中输出每次猜测相应的结果，直到输出猜对的结果或“Game Over”则结束。 输入样例: 58 4 70 50 56 58 60 -2 输出样例: Too big Too small Too small Good Guess! 参考答案： #include<iostream> using namespace std; int main(){ int target,N; cin>>target>>N; int num; cin>>num; int cnt = 1; while(num>=0){ if(cnt>N){ cout<<"Game Over"; return 0; } if(num==target){ if(cnt == 1){ cout<<"Bingo!"; return 0; } else if(cnt<=3){ cout<<"Lucky You!"; return 0; } else if(cnt<=N){ cout<<"Good Guess!"; return 0; } } else if(num>target){ cout<<"Too big"<<endl; cnt++; }else { cout<<"Too small"<<endl; cnt++; } cin>>num; } cout<<"Game Over"; return 0; } 四. 7-4 阶梯电价 为了提倡居民节约用电，某省电力公司执行“阶梯电价”，安装一户一表的居民用户电价分为两个“阶梯”：月用电量50千瓦时（含50千瓦时）以内的，电价为0.53元/千瓦时；超过50千瓦时的，超出部分的用电量，电价上调0.05元/千瓦时。…

pta实验一
type: item
date: 2025-11-14T22:18:00+08:00
description: 实验一 一. 7-1 3人分糖果 幼儿园里，有3个小朋友编号分别是1、2、3，他们按自己的编号顺序围坐在一张圆桌旁，每个小朋友的面前分别有8、9、10颗糖果。现在做一个分糖果游戏，从1号小朋友开始，将自己的糖果平均分成三份（如果有多余的糖果，则自己立刻吃掉），自己留一份，其余两份分给相邻座位的两个小朋友。接着，2号、3号小朋友也同样这么做。请问一轮后，每个小朋友面前分别有多少糖果？ 输入格式: 本题目没有输入。 输出格式: 在一行内显示以下结果。注意行末除了换行符不能有任何多余字符。 10 8 5 输入样例: 本题目没有输入。 输出样例: 10 8 5 参考答案： #include<iostream> using namespace std; int main(){ cout<<10<<" "<<8<<" "<<5; return 0; } 二. 什么是机器学习 什么是机器学习？上图展示了一段面试官与“机器学习程序”的对话： 面试官：9 + 10 等于多少？ 答：3 面试官：差远了，是19。 答：16 面试官：错了，是19。 答：18 面试官：不，是19。 答：19 本题就请你模仿这个“机器学习程序”的行为。 输入格式： 输入在一行中给出两个整数，绝对值都不超过 100，中间用一个空格分开，分别表示面试官给出的两个数字 A 和 B。 输出格式： 要求你输出 4 行，每行一个数字。第 1 行比正确结果少 16，第 2 行少 3，第 3 行少 1，最后一行才输出 A+B 的正确结果。 输入样例： 9 10 输出样例： 3 16 18 19 参考答案： #include<iostream> using namespace std; int main(){ int a,b; cin>>a>>b; int sum = a+b; cout<<sum-16<<endl; cout<<sum-3<<endl; cout<<sum-1<<endl; cout<<sum; return 0; } 三. 7-3 鸡兔同笼 《孙子算经》中记载了一个有趣的问题：“今有雉兔同笼，上有三十五头，下有九十四足，问雉兔各几何？”根据上述描述可列出如下形式的二元一次方程组。 {x+y=352x+4y=94​ 那么求解鸡兔同笼问题就转化成了二元一次方程组求解问题。本题要求编写程序对任意给定的头数和足数判断鸡和兔各有多少。 输入格式: 在一行中给出任意两个非负整数，整数之间用空格分开，第一个整数表示头数，第二个整数表示足数。 输出格式: 在一行中按照chickens = mm; rabbits = nn的格式输出解，如果无解则输出No solution! 。其中mm与nn都是非负整数。 输入样例1: 35 94 输出样例1: chickens = 23; rabbits = 12 输入样例2: 94 35 输出样例1: No solution! 参考答案 #include<iostream> using namespace std; int main(){ int head,foots; cin>>head>>foots; if((foots-head*2)%2!=0 || (head==0&&foots!=0)||(foots==0&&head!=0)||head<foots/4){ cout<<"No solution!"; return 0; } int rabbits = (foots-head*2)/2; int chickens = head - rabbits; cout<<"chickens = "<<chickens<<"; rabbits = "<<rabbits; return 0; } 四. 7-4 位数和 输入一个4位整数，求各个位上的数字之和。 输入格式: 一个四位数的整数。 输出格式: 一个整数，表示各位之和。 输入样例: 1234 输出样例: 10 样例解释：1+2+3+4=10 参考答案 #include<iostream> using namespace std; int main(){ int num; cin>>num; int sum = 0; while(num>0){ int left = num%10; num/=10; sum+=left; } cout<<sum; return 0; } 五. 7-5 逆序的三位数 程序每次读入一个正3位数，然后输出按位逆序的数字。注意：当输入的数字含有结尾的0时，输出不应带有前导的0。比如输入700，输出应该是7。 输入格式： 每个测试是一个3位的正整数。 输出格式： 输出按位逆序的数。 输入样例： 123 输出样例： 321 参考答案 #include<iostream> us…

Lua脚本解决高并发与库存超卖
type: item
date: 2025-07-15T21:22:25+08:00
description: Lua脚本解决高并发与库存超卖 尽管从Redis6开始，网络I/O可以开启多线程模式（通过io-threads配置项），但主命令执行仍然保持单线程。因此，利用主命令执行单线程的性质，可以模拟高并发环境下加锁的处理，加上redis本身具有的超快处理优势，可以完美替代程序中加锁的逻辑。 具体流程图如下所示： 1.编写controller用例： package com.example.demo; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.*; @RestController public class LuaController { private static final String Lua_SCRIPT = """ if tonumber(redis.call('exists',KEYS[1])) == 0 then redis.call('set',KEYS[1],'10') end if tonumber(redis.call('exists',KEYS[2])) == 0 then redis.call('sadd',KEYS[2],'-1') end if tonumber(redis.call('get',KEYS[1]))>0 and tonumber(redis.call('sismember',KEYS[2],ARGV[1])) == 0 then redis.call('incrby',KEYS[1],'-1') redis.call('sadd',KEYS[2],ARGV[1]) return 1 else return 0 end """; private final StringRedisTemplate stringRedisTemplate; LuaController(StringRedisTemplate redisTemplate){ this.stringRedisTemplate = redisTemplate; } @GetMapping("sk") public Map<String,String> seckill(String pid){ Map<String,String> resp = new HashMap<>(); String uid = String.valueOf(new Random().nextInt(10000000)); List<String> keys = new ArrayList<>(); keys.add("P"+pid); keys.add("U"+pid); DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(Lua_SCRIPT,Long.class); Long result = stringRedisTemplate.execute(redisScript,keys,uid); resp.put("uid",uid); resp.put("result",String.valueOf(result)); return resp; } } 其中，以productid为“P1001”为例： if tonumber(redis.call('exists',KEYS[1])) == 0 then redis.call('set',KEYS[1],'10') end if tonumber(redis.call('exists',KEYS[2])) == 0 then redis.call('sadd',KEYS[2],'-1') end 这一段lua脚本表示如果当前redis没有存入“P1001”的商品，就初始化“P1001”=10，表示“P1001”的商品库存有10个，等待秒杀。 KEYS[2]，即uid的列表，比如“U1001”，值是一个set集合，存储秒杀成功的用户id，比如{“1009099”，“90909009”，“68676889” .......}，如果不…

权限操作：RBAC+ACL搭配
type: item
date: 2025-05-04T10:12:37+08:00
description: 权限操作：RBAC+ACL搭配 1. RBAC(基于角色的访问控制) RBAC将权限分配给角色，用户通过关联角色间接获得权限。系统通过“用户-角色-权限”的层级关系实现权限管理。 这种经典的权限控制设计有一个缺点，就是难以处理特殊个体的细粒度权限，比如我作为管理员，我需要将某个文件的访问权限交给不是管理员，但是信任的人(比如张三)进行访问。 2. ACL(访问控制列表) ACL直接在资源上定义允许操作的主体，每条记录明确指定"谁可以对某资源做什么" 这种划分权限类似于我们平常使用的windows文件管理的方式，其主要优势在于灵活性强，每条权限规则都明确可见。但是缺点也很明显，那就是当用户数量或者资源数量很大的时候，ACL条目呈现指数级增长。 3. RBAC 与 ACL 联合使用的优势 灵活性与效率的平衡 RBAC 处理通用权限（如部门角色、职级权限） ACL 处理例外场景（如特殊用户需要访问特定资源） 比如，在企业系统中，普通员工通过角色获得基础权限，但某个机密文件可通过ACL单独授权给少数高管。 减少角色爆炸 通过ACL补充RBAC，避免在RBAC中因为个别的特殊权限去创建冗余角色 精细化权限管理 RBAC管理粗粒度权限（如”访问财务模块“），ACL控制细粒度操作（如”仅能查看报表，不能导出“） 冲突解决与优先级 当RBAC和ACL权限有冲突的时候，可以设定优先级，比如ACL优先于RBAC 4. RBAC + ACL模型举例 RBAC（基于角色的访问控制）经典模型中通常包含以下5张核心表： -- 1. 用户表（3个用户） CREATE TABLE users ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100) ); INSERT INTO users (username, email) VALUES ('alice', 'alice@example.com'), -- admin ('bob', 'bob@example.com'), -- user ('charlie', 'charlie@example.com'); -- user -- 2. 角色表（admin 和 user） CREATE TABLE roles ( role_id INT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL UNIQUE ); INSERT INTO roles (role_name) VALUES ('admin'), ('user'); -- 3. 权限表（只包含读和写） CREATE TABLE permissions ( permission_id INT PRIMARY KEY AUTO_INCREMENT, permission_key VARCHAR(50) NOT NULL UNIQUE ); INSERT INTO permissions (permission_key) VALUES ('article:read'), ('article:write'); -- 4. 用户-角色关联表 CREATE TABLE user_roles ( user_id INT, role_id INT, PRIMARY KEY (user_id, role_id), FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) ); -- 分配角色（1个admin，2个user） INSERT INTO user_roles (user_id, role_id) VALUES (1, 1), -- alice 是 admin (2, 2), -- bob 是 user (3, 2); -- charlie 是 user -- 5. 角色-权限关联表 CREATE TABLE role_permissions ( role_id INT, permission_id INT, PRIMARY KEY (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ); -- 权限分配：admin有读写，user只有读 INSERT INTO role_permissions (role_id,…

微服务架构-Outbox发件箱模式
type: item
date: 2025-04-25T17:14:16+08:00
description: 微服务架构-Outbox发件箱模式 背景：在现代微服务架构中，服务之间常常需要通过异步消息传递来解耦。例如，一个订单服务创建订单后，需要通知库存服务扣减库存。如何在不同服务间传递事件的同时保持数据一致性，成为了分布式系统设计的核心挑战之一。 本篇将介绍一种经典的一致性设计模式：Outbox（发件箱）模式。它能有效解决“写库成功但消息发送失败”所带来的数据不一致问题。 1. 为什么需要Outbox模式？ 考虑一个场景的场景：订单服务先写入数据库，再发送消息的MQ通知库存服务扣减库存。 public void createOrder(Order order) { orderRepository.save(order); // 写数据库 messageQueue.send("order_created", order); // 发消息 } 如果第2步消息发送失败（比如MQ挂掉了、网络波动），订单已经创建了，但是库存不会减少，就会导致业务不一致，可能这时我们就需要进行回滚或者补偿机制，但是我们这次介绍的Outbox设计模式提供了一个全新的思路，确保在第一次处理时就保证“本地事务+跨服务通信”下的数据一致性问题。 2. Outbox模式架构详解 2.1. 核心思想 在数据库中新增一个outbox表，用于保存待发送的事件。 在本地事务中，同时写入业务数据和outbox事件。 通过后台轮询工具或者定时脚本读取outbox表中的事件，找到状态为待发送的事件，发送消息。 2.2. Outbox模式流程图 3. 代码示例 3.1. 创建订单代码（写入Outbox） @Transactional public void createOrder(Order order) { // 1. 创建订单记录 orderRepository.save(order); // 2. 写入发件箱表 OutboxEvent event = new OutboxEvent("ORDER_CREATED", JsonUtil.toJson(order)); outboxRepository.save(event); } 3.2. 消息发送器（定时轮询版） @Scheduled(fixedDelay = 5000) public void dispatchEvents() { List<OutboxEvent> events = outboxRepository.findPendingEvents(); for (OutboxEvent event : events) { try { messageQueue.send(event.getType(), event.getPayload()); event.markAsSent(); outboxRepository.save(event); } catch (Exception e) { // 这里可以放补偿方法或者最终的回滚机制，也可以放重试机制 } } } 4. 适用场景与总结 Outbox模式适用于： 核心业务链路必须保证事件可靠传递 有本地数据库，且可控制写入事务（如 MYSQL） 可以接受最终一致性，不追求强一致性 不适合： 无本地数据库的场景（如纯计算服务） 要求毫秒级响应的一致性事务 总结 Outbox模式是解决分布式事务一致性的一个经典方法。通过“写数据库 + 发消息”组合的原子化改造，把事件的可靠投递交由后台发送器完成，极大地提升了系统的容错性和可靠性。 在电商订单、支付系统、库存调整等典型的微服务场景中，Outbox都是一个值得考虑的模式。

设计模式原则分析
type: item
date: 2024-12-16T20:53:38+08:00
description: 设计模式原则分析 1. 单一职责原则（Single Responsibility Principle） 1.1 定义 单一职责原则是指一个类应该只有一个责任（即一个功能），一个类应该只负责一项任务。换句话说，如果一个类承担了多个职责，这些职责之间会有不同的变化原因，改变其中某个职责时，可能会影响到其他职责，导致系统更加复杂和难以维护。 1.2 不符合SRP原则举例 假设有一个类同时处理数据存储和报告生成的功能。当存储方式变化时（如从数据库变为文件存储），我们需要修改这个类的存储相关代码；而当报告生成的需求变化时（如格式从 PDF 变为 Excel），我们也需要修改这个类的报告生成代码。此时，这个类有两个职责，它们各自有不同的变化原因，导致这个类的维护变得复杂。 // 违反 SRP 原则：一个类同时负责数据存储和报告生成 public class UserManager { public void saveUser(User user) { // 保存用户到数据库 System.out.println("Saving user to database"); } public void generateUserReport(User user) { // 生成用户报告 System.out.println("Generating report for user"); } } 1.3 遵守SRP原则举例 // 遵守 SRP 原则：将职责分离到不同的类中 public class UserRepository { public void saveUser(User user) { // 保存用户到数据库 System.out.println("Saving user to database"); } } public class UserReportGenerator { public void generateUserReport(User user) { // 生成用户报告 System.out.println("Generating report for user"); } } 在这个例子中，UserRepository 只负责数据存储，而 UserReportGenerator 只负责生成报告。这样，每个类只负责一个功能，系统的职责划分更加清晰，修改某一功能时只需要修改对应的类，而不影响其他部分。 2. 开闭原则（Open/Closed Principle） 2.1 定义 开放-封闭原则指的是软件实体（类、模块、函数等）应该对扩展开放，对修改封闭。这意味着当我们需要为系统添加新功能时，应该通过扩展已有的代码，而不是直接修改已有的代码。 2.2 不符合OCP原则举例 假设我们有一个计算薪水的程序，原本只考虑基本工资和奖金两个因素。如果以后需要加入更多的薪酬计算因素（如补贴、扣款等），如果我们每次都修改原有的计算逻辑，就会违反开放-封闭原则。 // 违反 OCP 原则：修改原有类以支持新的薪资计算方式 public class SalaryCalculator { public double calculateSalary(Employee employee) { // 计算基本工资和奖金 return employee.getBaseSalary() + employee.getBonus(); } } 假设现在我们需要新增一个“补贴”因素，每次添加新因素时都需要修改 SalaryCalculator 类的 calculateSalary 方法。随着需求变化，SalaryCalculator 类会变得越来越复杂，而且容易出错。这就是违反了开放-封闭原则。 2.3 符合OCP原则举例 为了遵守开放-封闭原则，我们可以引入抽象类或接口，将不同的薪资计算策略封装成不同的类，而不是修改 SalaryCalculator 类。 // 遵守 OCP 原则：将薪资计算逻辑通过策略模式封装，便于扩展 public interface SalaryCalculatorStrategy { double calculateSalary(Employee employee); } public class BaseSalaryAndBonusCalculator implements SalaryCalculatorStrategy { @Override public double calculateSalary(Employee employee) { // 计算基本工资和奖金 return employee.getBaseSalary() + employee.getBonus(); } } public class BaseSalaryBon…