首页
视频
留言
壁纸
直播
下载
友链
统计
推荐
vue
在线工具
Search
1
记一个报错GC overhead limit exceeded解决方法
1,023 阅读
2
ElasticSearch ES 安装 Kibana安装 设置密码
785 阅读
3
Teamcity + Rancher + 阿里云Code 实现Devops 自动化部署
475 阅读
4
解决Mybatis-Plus批量插入数据太慢,堪称神速
418 阅读
5
分布式锁Redisson,完美解决高并发问题
396 阅读
JAVA开发
前端相关
Linux相关
电商开发
经验分享
电子书籍
个人随笔
行业资讯
其他
登录
/
注册
Search
标签搜索
AOP
支付
小说
docker
SpringBoot
XML
秒杀
K8S
RabbitMQ
工具类
Shiro
多线程
分布式锁
Redisson
接口防刷
Jenkins
Lewis
累计撰写
146
篇文章
累计收到
14
条评论
首页
栏目
JAVA开发
前端相关
Linux相关
电商开发
经验分享
电子书籍
个人随笔
行业资讯
其他
页面
视频
留言
壁纸
直播
下载
友链
统计
推荐
vue
在线工具
搜索到
144
篇与
的结果
2021-10-21
把Windows 11恢复成Windows 10中右键菜单的外观(修改注册表实现)
{card-default label="实现方案" width=""}1.按Windows + R键激活“运行”对话框。2.键入regedit并按Enter键,启动注册表编辑器。3.导航到HKEY_CURRENT_USER\Software\Classes\CLSID4.单击编辑>新建>项,创建一个名为{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}的新建项。5.选择新建立的项,单击编辑>新建>项,并创建一个名为InprocServer32的新建项。6.选择新创建的项,然后双击右侧窗格中的默认条目。7.不要输入任何内容,但按Enter(你应该注意到数据列从(未设置值)变为空白)。8.要么重新启动计算机,要么重新启动explorer.exe进程。{/card-default}效果图:
2021年10月21日
109 阅读
0 评论
0 点赞
2021-10-21
前端一些实用的函数
/** * @description: 滚动至页面顶部 * @param {*} * @return {*} */ const goToTop = () => window.scrollTo(0, 0); /** * @description: 校验身份证 * @param {*} * @return {*} */ export const validateIDCard = value => /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(value); /** * @description: 校验支付宝账号 * @param {*} * @return {*} */ export const validateAlipay = value => /^1\d{10}$|^[a-zA-Z\d._-]*\@[a-zA-Z\d.-]{1,10}\.[a-zA-Z\d]{1,20}$/.test(value); /** * @description: 校验银行卡 * @param {*} * @return {*} */ export const validateBankCode = value => /^\d{13,19}$/.test(value); /** * @description: 校验手机号 * @param {*} * @return {*} */ export const validatePhone = value => /^1\d{10}$/.test(value); /** * @description: 函数节流 * @param {*} * @return {*} */ export const throttle = function (fn, delay = 1000) { let prev = 0; return function () { const now = Date.now(); if (now - prev > delay) { fn.apply(this, arguments); prev = Date.now(); } } } /** * @description: 获取随机字符串 * @param {*} * @return {*} */ export const randomString = () => Math.random().toString(36).substr(2); /** * @description: 将 BASE64 转换文件 * @param {*} * @return {*} */ export const dataURLtoFile = (dataurl, filename) => { const arr = dataurl.split(','); const mime = arr[0].match(/:(.*?);/)[1]; if (!filename) filename = `${Date.parse(new Date())}.jpg`; const bstr = window.atob(arr[1]); let n = bstr.length; const u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([u8arr], filename, { type: mime }); } /** * @description: 压缩图片 * @param {*} * @return {*} */ export const compressImg = file => { const fileSize = parseFloat(Number.parseInt(file.size, 10) / 1024 / 1024).toFixed(2); const reader = new FileReader(); reader.readAsDataURL(file); return new Promise((resolve) => { reader.onload = e => { const img = new Image(); img.src = e.target.result; img.onload = () => { const w = img.width; const h = img.height; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); let base64; canvas.setAttribute('width', w); canvas.setAttribute('height', h); ctx.drawImage(img, 0, 0, w, h); if (fileSize <= 1) { base64 = canvas.toDataURL(file.type, 1); } else if (fileSize <= 3) { base64 = canvas.toDataURL(file.type, 0.8); } else if (fileSize <= 5) { base64 = canvas.toDataURL(file.type, 0.5); } else { base64 = canvas.toDataURL(file.type, 0.1); } let fileName = file.name; fileName = fileName.replace(/^(.+)\.(.+)$/, (fullName, name, suffix) => name + Math.floor(Math.random() * (9999 - 1000) + 1000) + '.' + suffix); resolve(dataURLtoFile(base64, fileName)); }; }; }); } /* 防抖原理:在一定时间内,只有最后一次操作,再过wait毫秒后才执行函数 @param {Function} func 要执行的回调函数 @param {Number} wait 延迟的时间 @param{Boolean} immediate 是否要立即执行 */ let timeout = null; function debounce(func, wait = 500, immediate = false) { // 清除定时器 if (timeout !== null) clearTimeout(timeout); // 立即执行,此类情况一般用不到 if (immediate) { var callNow = !timeout; timeout = setTimeout(() => { timeout = null; }, wait); if (callNow) typeof func === "function" && func(); } else { // 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法 timeout = setTimeout(() => { typeof func === "function" && func(); }, wait); } } export default debounce; /** * @description: 节流 * 节流原理:在一定时间内,只能触发一次 * @param {Function} func 要执行的回调函数 * @param {Number} wait 延时的时间 * @param {Boolean} immediate 是否立即执行 * @return null */ let timer, flag; function throttle(func, wait = 500, immediate = true) { if (immediate) { if (!flag) { flag = true; // 如果是立即执行,则在wait毫秒内开始时执行 typeof func === 'function' && func(); timer = setTimeout(() => { flag = false; }, wait); } } else { if (!flag) { flag = true // 如果是非立即执行,则在wait毫秒内的结束处执行 timer = setTimeout(() => { flag = false typeof func === 'function' && func(); }, wait); } } }; export default throttle /** * time 任何合法的时间格式、秒或毫秒的时间戳 * format 时间格式,可选。默认为yyyy-mm-dd,年为"yyyy",月为"mm",日为"dd",时为"hh",分为"MM",秒为"ss",格式可以自由搭 **/ function timeFormat(dateTime = null, fmt = 'yyyy-mm-dd') { // 如果为null,则格式化当前时间 if (!dateTime) dateTime = Number(new Date()); // 如果dateTime长度为10或者13,则为秒和毫秒的时间戳,如果超过13位,则为其他的时间格式 if (dateTime.toString().length == 10) dateTime *= 1000; let date = new Date(dateTime); let ret; let opt = { "y+": date.getFullYear().toString(), // 年 "m+": (date.getMonth() + 1).toString(), // 月 "d+": date.getDate().toString(), // 日 "h+": date.getHours().toString(), // 时 "M+": date.getMinutes().toString(), // 分 "s+": date.getSeconds().toString() // 秒 // 有其他格式化字符需求可以继续添加,必须转化成字符串 }; for (let k in opt) { ret = new RegExp("(" + k + ")").exec(fmt); if (ret) { fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0"))) }; }; return fmt; } export default timeFormat /** *手机号加密 */ export const phoneFormat = (phone = 0) => { return String(phone).replace(/^(\d{3})\d{4}(\d+)$/, '$1****$2'); }; /** * 验证手机号 */ const REGEXP_PHONE = /^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/; /** * 验证身份证号 */ const REGEXP_IDCARD = /^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/; /** * 验证电子邮箱格式 */ function email(value) { return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value); } /** * 验证手机格式 */ function mobile(value) { return /^1[23456789]\d{9}$/.test(value) } /** * 验证URL格式 */ function url(value) { return /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w-.\/?%&=]*)?/.test(value) } /** * 验证日期格式 */ function date(value) { return !/Invalid|NaN/.test(new Date(value).toString()) } /** * 是否车牌号 */ function carNo(value) { // 新能源车牌 const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/; // 旧车牌 const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/; if (value.length === 7) { return creg.test(value); } else if (value.length === 8) { return xreg.test(value); } else { return false; } } /** * 金额,只允许2位小数 */ function amount(value) { //金额,只允许保留两位小数 return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value); } /** *只能输入字母 */ function letter(value) { return /^[a-zA-Z]*$/.test(value); } /** *只能是字母或者数字 */ function enOrNum(value) { //英文或者数字 let reg = /^[0-9a-zA-Z]*$/g; return reg.test(value); } /** * 是否固定电话 */ function landline(value) { let reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/; return reg.test(value); } /** * 是否json字符串 */ function jsonString(value) { if (typeof value == 'string') { try { var obj = JSON.parse(value); if (typeof obj == 'object' && obj) { return true; } else { return false; } } catch (e) { return false; } } return false; } /** * 密码强度校验 * 说明:密码中必须包含字母、数字、特称字符,至少8个字符,最多30个字符 */ const passwordReg = /(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}/ const password1 = 'sunshine_Lin12345..' console.log(passwordReg.test(password1)) // true const password2 = 'sunshineLin12345' console.log(passwordReg.test(password2)) // false /** * 全局唯一标识符 * guid(length = 32, firstU = true, radix = 62) * length <Number | null> guid的长度,默认为32,如果取值null,则按rfc4122标准生成对应格式的随机数 * firstU 首字母是否为"u",如果首字母为数字情况下,不能用作元素的id或者class,默认为true * radix 生成的基数,默认为62,用于生成随机数字符串 */ function guid(len = 32, firstU = true, radix = null) { let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); let uuid = []; radix = radix || chars.length; if (len) { // 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位 for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix]; } else { let r; // rfc4122标准要求返回的uuid中,某些位为固定的字符 uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; uuid[14] = '4'; for (let i = 0; i < 36; i++) { if (!uuid[i]) { r = 0 | Math.random() * 16; uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; } } } // 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class if (firstU) { uuid.shift(); return 'u' + uuid.join(''); } else { return uuid.join(''); } } export default guid; /** * 对象转URL参数 * queryParams(data, isPrefix = true, arrayFormat = 'brackets') */ /** * 对象转url参数 * @param {*} data,对象 * @param {*} isPrefix,是否自动加上"?" */ function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') { let prefix = isPrefix ? '?' : '' let _result = [] if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets'; for (let key in data) { let value = data[key] // 去掉为空的参数 if (['', undefined, null].indexOf(value) >= 0) { continue; } // 如果值为数组,另行处理 if (value.constructor === Array) { // e.g. {ids: [1, 2, 3]} switch (arrayFormat) { case 'indices': // 结果: ids[0]=1&ids[1]=2&ids[2]=3 for (let i = 0; i < value.length; i++) { _result.push(key + '[' + i + ']=' + value[i]) } break; case 'brackets': // 结果: ids[]=1&ids[]=2&ids[]=3 value.forEach(_value => { _result.push(key + '[]=' + _value) }) break; case 'repeat': // 结果: ids=1&ids=2&ids=3 value.forEach(_value => { _result.push(key + '=' + _value) }) break; case 'comma': // 结果: ids=1,2,3 let commaStr = ""; value.forEach(_value => { commaStr += (commaStr ? "," : "") + _value; }) _result.push(key + '=' + commaStr) break; default: value.forEach(_value => { _result.push(key + '[]=' + _value) }) } } else { _result.push(key + '=' + value) } } return _result.length ? prefix + _result.join('&') : '' } export default queryParams; console.log(queryParams({name:'li',age:20})) // 结果:?name=li&age=20 const data = {name: '冷月夜',fruits: ['apple', 'banana', 'orange']} queryParams(this.data, true, 'indices'); // 结果为:?name=冷月夜&fruits[0]=apple&fruits[1]=banana&fruits[2]=orange queryParams(this.data, true, 'brackets'); // 结果为:?name=冷月夜&fruits[]=apple&fruits[]=banana&fruits[]=orange queryParams(this.data, true, 'repeat'); // 结果为:?name=冷月夜&fruits=apple&fruits=banana&fruits=orange queryParams(this.data, true, 'comma'); // 结果为:?name=冷月夜&fruits=apple,banana,orange /** * 数组排序 * sort排序 */ // 对数字进行排序,简写 const arr = [3, 2, 4, 1, 5] arr.sort((a, b) => a - b) console.log(arr) // [1, 2, 3, 4, 5] // 对字母进行排序,简写 const arr = ['b', 'c', 'a', 'e', 'd'] arr.sort() console.log(arr) // ['a', 'b', 'c', 'd', 'e'] /** * 数组排序 * 冒泡排序 */ // 对数字进行排序,简写 const arr = [3, 2, 4, 1, 5] arr.sort((a, b) => a - b) console.log(arr) // [1, 2, 3, 4, 5] // 对字母进行排序,简写 const arr = ['b', 'c', 'a', 'e', 'd'] arr.sort() console.log(arr) // ['a', 'b', 'c', 'd', 'e'] /** * 获取URL参数 * URLSearchParams 方法 */ // 创建一个URLSearchParams实例 const urlSearchParams = new URLSearchParams(window.location.search); // 把键值对列表转换为一个对象 const params = Object.fromEntries(urlSearchParams.entries()); // split方法 function getParams(url) { const res = {} if (url.includes('?')) { const str = url.split('?')[1] const arr = str.split('&') arr.forEach(item => { const key = item.split('=')[0] const val = item.split('=')[1] res[key] = decodeURIComponent(val) // 解码 }) } return res } // 测试 const user = getParams('http://www.baidu.com?user=%E9%98%BF%E9%A3%9E&age=16') console.log(user) // { user: '阿飞', age: '16' }
2021年10月21日
71 阅读
0 评论
0 点赞
2021-10-21
安装Gitlab到CentOS(YUM)
GitLab是Git的基于WEB的图形化管理平台,提供Git的用户、权限等高级管理功能。 {card-default label="运行环境" width=""}系统版本:CentOS Linux release 7.3.1611 (Core)软件版本:Gitlab-ce-11.10.1硬件要求:最低2核4GB,建议4核8GB{/card-default}安装过程1、安装依赖[root@localhost ~]# yum -y install curl policycoreutils-python openssh-server 2、配置系统环境[root@localhost ~]# systemctl enable sshd => 设置SSH远程服务开机自启 [root@localhost ~]# systemctl start sshd => 启动SSH远程服务 [root@localhost ~]# systemctl stop firewalld => 停止Firewalld防火墙服务 [root@localhost ~]# systemctl disable firewalld => 禁用Firwalld防火墙服务开机自启 [root@localhost ~]# sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/sysconfig/selinux => 关闭SeLinux(重启主机生效) [root@localhost ~]# setenforce 0 => 关闭SeLinux(当前生效)3、添加YUM-Gitlab源我们使用清华大学提供的YUM源,以提高下载速度。[root@localhost ~]# vim /etc/yum.repos.d/gitlab-ce.repo [gitlab-ce] name=Gitlab CE Repository baseurl=https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el$releasever/ gpgcheck=0 enabled=1 [root@localhost ~]# yum makecache4、安装Gitlab我们选择安装最新版本的Gitlab。[root@localhost ~]# yum install -y gitlab-ce可以访问"https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/"查看Gitlab-ce的版本。安装历史版本请使用下面命令:[root@localhost ~]# yum install -y gitlab-ce-{VERSION}5、配置Gitlab建议使用HTTPS。[root@localhost ~]# vim /etc/gitlab/gitlab.rb ### 基础配置 ### external_url 'https://gitlab.xxx.cn' #用户访问所使用的URL,域名或者IP地址 gitlab_rails['time_zone'] = 'Asia/Shanghai' #时区 ### SSH配置 ### gitlab_rails['gitlab_shell_ssh_port'] = 10222 #使用SSH协议拉取代码所使用的连接端口。 ### 邮箱配置 ### gitlab_rails['smtp_enable'] = true #启用SMTP邮箱功能,绑定一个第三方邮箱,用于邮件发送 gitlab_rails['smtp_address'] = "smtp.exmail.qq.com" #设置SMTP服务器地址 gitlab_rails['smtp_port'] = 465 #设置SMTP服务器端口 gitlab_rails['smtp_user_name'] = "xxx@xxx.cn" #设置邮箱账号 gitlab_rails['smtp_password'] = "xxx" #设置邮箱密码 gitlab_rails['smtp_authentication'] = "login" #设置邮箱账号密码身份验证方式,"login"表示采用账号密码的方式登陆 gitlab_rails['smtp_enable_starttls_auto'] = true gitlab_rails['smtp_tls'] = true #设置开启SMTP邮件使用TLS传输加密协议传输邮件,以保证邮件安全传输 gitlab_rails['gitlab_email_from'] = 'xxx@xxx.cn' #设置Gitlab来源邮箱地址,设置登陆所使用的邮箱地址 ### WEB配置 ### nginx['enable'] = true #启用Nginx服务 nginx['client_max_body_size'] = '250m' #设置客户端最大文件上传大小 nginx['redirect_http_to_https'] = true #设置开启自动将HTTP跳转到HTTPS nginx['ssl_certificate'] = "/etc/gitlab/ssl/gitlab.xxx.cn.pem" #设置HTTPS所使用的证书 nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/gitlab.xxx.cn.key" #设置HTTPS所使用的证书密码 nginx['ssl_protocols'] = "TLSv1.1 TLSv1.2" #设置HTTPS所使用的TLS协议版本 nginx['ssl_session_cache'] = "builtin:1000 shared:SSL:10m" #设置开启SSL会话缓存功能 nginx['ssl_session_timeout'] = "5m" #设置SSL会话超时时间 nginx['listen_addresses'] = ['*', '[::]'] #设置Nginx监听地址,"*"表示监听主机上所有网卡的地址 nginx['gzip_enabled'] = true #设置开启Nginx的传输压缩功能,以节约传输带宽,提高传输效率6、上传SSL证书到指定目录[root@localhost ~]# ll /etc/gitlab/ssl/ total 28 drwxr-xr-x 2 root root 4096 Apr 25 11:48 ./ drwxrwxr-x 4 root root 4096 Apr 25 12:50 ../ -rw-r--r-- 1 root root 1675 Apr 25 11:45 gitlab.xxx.cn.key -rw-r--r-- 1 root root 3671 Apr 25 11:45 gitlab.xxx.cn.pem7、刷新配置当配置文件发生变化时,或者是第一次启动时,我们需要刷新配置。[root@localhost ~]# systemctl restart gitlab-runsvdir [root@localhost ~]# gitlab-ctl reconfigure8、启动服务[root@localhost ~]# gitlab-ctl restart [root@localhost ~]# gitlab-ctl status run: alertmanager: (pid 13541) 2171s; run: log: (pid 13221) 2192s run: gitaly: (pid 13557) 2170s; run: log: (pid 12463) 2266s run: gitlab-monitor: (pid 13580) 2169s; run: log: (pid 13103) 2208s run: gitlab-workhorse: (pid 13602) 2169s; run: log: (pid 12887) 2226s run: logrotate: (pid 13617) 2168s; run: log: (pid 12959) 2218s run: nginx: (pid 13628) 2168s; run: log: (pid 12927) 2222s run: node-exporter: (pid 13714) 2168s; run: log: (pid 13002) 2214s run: postgres-exporter: (pid 13720) 2167s; run: log: (pid 13270) 2188s run: postgresql: (pid 13740) 2167s; run: log: (pid 12669) 2258s run: prometheus: (pid 13748) 2166s; run: log: (pid 13181) 2198s run: redis: (pid 13761) 2166s; run: log: (pid 11907) 2293s run: redis-exporter: (pid 13800) 2165s; run: log: (pid 13143) 2202s run: sidekiq: (pid 13821) 2163s; run: log: (pid 12872) 2227s run: unicorn: (pid 13833) 2162s; run: log: (pid 12832) 2233s9、测试邮件发送我们在启动完成后测试一下邮件发送功能是否正常工作。[root@localhost ~]# gitlab-rails console irb(main):001:0> Notify.test_email('邮箱地址', '标题', '内容').deliver_now irb(main):002:0> exit10、第一次访问登陆第一次需要输入新的超级管理员(root)密码。修改成功后,我们使用超级管理员用户“root”账号登录Gitlab管理平台。11、关闭用户注册功能为了避免用户随便注册账号,我们将注册功能关闭。11、设置语言为"简体中文"保存后重启登陆即可。
2021年10月21日
121 阅读
0 评论
0 点赞
2021-10-21
VUE 父子组件传值
{message type="success" content=" 一、父组件把值传给子组件 "/}{card-default label="实现原理" width=""}1.父组件 在引用子组件时,通过属性绑定(v-bind:)的形式,把需要传递给子组件的数据,传递到子组件内部,供子组件使用。2.把父组件传递过来的数据, 在 props数组 中定义一下组件中的 所有props 中的数据,都是通过父组件传递给子组件的props 中的数据都是只读的,无法重新赋值3.在该子组件中使用props数组 中定义好的数据{/card-default}// 父组件:father.vue <template> <div> <h1>父组件</h1> <router-view v-bind:fData="data1" :fMessage="data2"></router-view> </div> </template> <script> export default { data () { return { data1: '父组件数据data1', data2: '父组件数据data2', }; } } </script> // 子组件:son.vue <template> <div> <h1>子组件</h1> <p>下面是父组件传过来的数据</p> <p>第一个数据:{{fData}}</p> <p>第二个数据:{{fMessage}}</p> </div> </template> <script> export default { props: ['fData', 'fMessage'], data () { return { }; } } </script>{dotted startColor="#ff6c6c" endColor="#1989fa"/}{message type="success" content=" 二、父组件把方法传递给子组件 "/}{card-default label="实现原理" width=""}1.父组件向子组件传递方法,使用事件绑定机制 v-on,自定义一个事件属性,传递给子组件2.在子组件中定义一个方法,在方法中,利用 $emit 触发 父组件传递过来的,挂载在当前实例上的事件,还可以传递参数3.在子组件中调用定义的那个方法,就可以触发父组件传递过来的方法了{/card-default}// 父组件:father.vue <template> <div> <h1>父组件</h1> <router-view @show="showFather"></router-view> </div> </template> <script> export default { data () { return { }; }, methods: { showFather (a, b) { console.log('触发了父组件的方法' + '======' + a + '======' + b); } } } </script> // 子组件:son.vue <template> <div> <h1>子组件</h1> <Button type="primary" @click="sonClick">触发父组件方法</Button> </div> </template> <script> export default { data () { return { }; }, methods: { sonClick () { this.$emit('show', 111, 222); } } } </script> {dotted startColor="#ff6c6c" endColor="#1989fa"/}{message type="success" content=" 三、子组件通过事件调用向父组件传值 "/}{card-default label="实现原理" width=""}在子组件中,利用 $emit 触发 父组件传递过来的方法的时候,可以将子组件的数据当做参数传递给父组件{/card-default}// 父组件:father.vue <template> <div> <h1>父组件</h1> <router-view @show="showFather"></router-view> </div> </template> <script> export default { data () { return { fromSon1: '', fromSon2: '' }; }, methods: { showFather (a, b) { this.fromSon1 = a; this.fromSon2 = b; console.log('触发了父组件的方法' + '======' + a + '======' + b); } } } </script> // 子组件:son.vue <template> <div> <h1>子组件</h1> <Button type="primary" @click="sonClick">触发父组件方法</Button> </div> </template> <script> export default { props: ['fData', 'fMessage'], data () { return { sonMessage: '子组件数据sonMessage', sonData: '子组件数据sonData' }; }, methods: { sonClick () { this.$emit('show', this.sonMessage, this.sonData); } } } </script> {dotted startColor="#ff6c6c" endColor="#1989fa"/}{message type="success" content="四、父子组件之间相互传值 "/}// 父组件:father.vue <template> <div> <h1>父组件</h1> <Button type="primary" @click="getData">获取数据</Button> <router-view v-bind:fData="data1" :fMessage="data2" @show="showFather"></router-view> </div> </template> <script> export default { data () { return { data1: '父组件数据data1', data2: '父组件数据data2', fromSon1: '', fromSon2: '' }; }, methods: { showFather (a, b) { this.fromSon1 = a; this.fromSon2 = b; console.log('触发了父组件的方法' + '======' + a + '======' + b); }, getData () { console.log(this.fromSon1); console.log(this.fromSon2); } } } </script> // 子组件:son.vue <template> <div> <h1>子组件</h1> <p>下面是父组件传过来的数据</p> <p>第一个数据:{{fData}}</p> <p>第二个数据:{{fMessage}}</p> <Button type="primary" @click="sonClick">触发父组件方法</Button> </div> </template> <script> export default { props: ['fData', 'fMessage'], data () { return { sonMessage: '子组件数据sonMessage', sonData: '子组件数据sonData' }; }, methods: { sonClick () { this.$emit('show', this.sonMessage, this.sonData); } } } </script>
2021年10月21日
181 阅读
0 评论
1 点赞
2021-10-21
JAVA&JS 针对数组(集合)去重
一、针对JS{callout color="#f0ad4e"}判断数组中是否包含对象,有的话不加入到数组中{/callout}// 判断数组中是否包含对象 export const isHasObj = (arr, item) => { let flag = false// true为有 false为没有 for (var i = 0; i < arr.length; i++) { if (JSON.stringify(arr[i]).indexOf(JSON.stringify(item)) !== -1) { flag = true } } return flag }二、针对JAVAlist.stream().distinct().collect(Collectors.toList())
2021年10月21日
65 阅读
0 评论
0 点赞
2021-10-14
三个场景,让你了解JAVA多线程
Java多线程是考量一个Java中级研发工程师的重要指标之一,小编通过几个典型的场景,以故事的形式,将Java多线程中的要点呈现给各位看客。Java多线程主要涉及到的编程技术有以下五点:对同一个变量进行操作对同一个对象进行操作回调方法使用线程同步,死锁问题线程通信场景一:电影院门口场景二:银行里的钱两个人AB,使用一个账户,A在柜台取钱和B在ATM机取钱程序分析:钱的数量要设置成一个静态的变量。两个人要取的同一个对象值故事三:龟兔赛跑龟兔赛跑:20米 //只要为了看到效果,所有距离缩短了要求:1.兔子每秒3米的速度,每跑6米休息10秒,2.乌龟每秒跑1米,不休息3.其中一个跑到终点后另一个不跑了!程序设计思路:1.创建一个Animal动物类,继承Thread,编写一个running抽象方法,重写run方法,把running方法在run方法里面调用。2.创建Rabbit兔子类和Tortoise乌龟类,继承动物类3.两个子类重写running方法4.本题的第3个要求涉及到线程回调。需要在动物类创建一个回调接口,创建一个回调对象
2021年10月14日
93 阅读
0 评论
0 点赞
2021-10-13
弹窗复制提示、文章自带版权说明
一、弹窗提示Javascript 代码实现将代码添加到 html 中即可,比如添加到模板的 footer.php、header.php。<script type="text/javascript"> document.body.oncopy=function(){alert('复制成功!本站文章皆为原创,未经允许禁止转载或抄袭,若要转载请务必保留原文链接谢谢合作!');} </script> 二、添加文章版权说明复制文章内容后不会有任何提示,但是粘贴时会自动把文章链接加到复制的内容后面。这段js代码我是放在post.php的文章内页php<script type="text/javascript"> function addLink() { var body_element = document.getElementsByTagName('body')[0]; var selection; selection = window.getSelection(); var pagelink = " 来自: <a href='"+document.location.href+"'>"+document.location.href+"</a>"; var copy_text = selection + pagelink; var new_div = document.createElement('div'); new_div.style.left='-99999px'; new_div.style.position='absolute'; body_element.appendChild(new_div ); new_div.innerHTML = copy_text ; selection.selectAllChildren(new_div ); window.setTimeout(function() { body_element.removeChild(new_div ); },0); } document.oncopy = addLink; </script>
2021年10月13日
200 阅读
0 评论
0 点赞
2021-10-13
Typecho网站底部添加页面打开时间、在线人数
1.在functions.php添加以下内容//在线人数 function online_users() { $filename='online.txt'; //数据文件 $cookiename='Nanlon_OnLineCount'; //Cookie名称 $onlinetime=30; //在线有效时间 $online=file($filename); $nowtime=$_SERVER['REQUEST_TIME']; $nowonline=array(); foreach($online as $line){ $row=explode('|',$line); $sesstime=trim($row[1]); if(($nowtime - $sesstime)<=$onlinetime){ $nowonline[$row[0]]=$sesstime; } } if(isset($_COOKIE[$cookiename])){ $uid=$_COOKIE[$cookiename]; }else{ $vid=0; do{ $vid++; $uid='U'.$vid; }while(array_key_exists($uid,$nowonline)); setcookie($cookiename,$uid); } $nowonline[$uid]=$nowtime; $total_online=count($nowonline); if($fp=@fopen($filename,'w')){ if(flock($fp,LOCK_EX)){ rewind($fp); foreach($nowonline as $fuid=>$ftime){ $fline=$fuid.'|'.$ftime."\n"; @fputs($fp,$fline); } flock($fp,LOCK_UN); fclose($fp); } } echo "$total_online"; } /** * 加载时间 以下为添加内容 * @return bool */ function timer_start() { global $timestart; $mtime = explode( ' ', microtime() ); $timestart = $mtime[1] + $mtime[0]; return true; } timer_start(); function timer_stop( $display = 0, $precision = 3) { global $timestart, $timeend; $mtime = explode( ' ', microtime() ); $timeend = $mtime[1] + $mtime[0]; $timetotal = number_format( $timeend - $timestart, $precision ); $r = $timetotal < 1 ? $timetotal * 1000 . " ms" : $timetotal . " s"; if ( $display ) { echo $r; } return $r; } 2.在footer.php添加以下内容 <span>加载耗时:<?php echo timer_stop();?></span> <span>在线人数: <?php echo online_users() ?>人</span>3.效果如下
2021年10月13日
138 阅读
0 评论
0 点赞
2021-10-13
通过CSS代码实现网站变成灰色的方法
一、通过对css文件添加代码来实现对调用的css文件里添加一下css代码,可以实现网页变黑白,也就是网站变灰。方法1html { filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); -webkit-filter: grayscale(100%); }方法2html { -webkit-filter: grayscale(100%); -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter:progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); _filter:none; }二、在网页的标签内加入以下代码如果你不想改动CSS文件,你可以通过在网页头部中的标签内部加入内联CSS代码的形式实现网站网页变灰<style type="text/css"> html { filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); -webkit-filter: grayscale(100%);} </style>三、修改标签加入内联样式如里上面的两种方式都不喜欢,可以通过修改标签,以加入内联样式的方法,达到网页变灰的效果<html style="filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); -webkit-filter: grayscale(100%);">四、最终我采用的代码body *{ -webkit-filter: grayscale(100%); /* webkit */ -moz-filter: grayscale(100%); /*firefox*/ -ms-filter: grayscale(100%); /*ie9*/ -o-filter: grayscale(100%); /*opera*/ filter: grayscale(100%); filter:progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); filter:gray; /*ie9- */ }以上几种方法,都是通过CSS的滤镜来控制页面的显示而已,唯一不同的就CSS代码调用的方式,根据实际的需要选择添加到代码中。
2021年10月13日
67 阅读
0 评论
0 点赞
2021-09-13
分布式锁Redisson,完美解决高并发问题
我们的活动遇到了性能问题,原先的单机锁性能太差,以下单购买商品为例,我们考虑使用分布式锁,但传统的方式为了使用Redis锁,我们需要设置一个定长的key,然后当购买完成后,将key删除。但为了防止key提前过期,我们不得不新增一个线程执行定时任务。现在我们可以使用Redissson框架简化代码。getLock()方法代替了Redis的setIfAbsent(),lock()设置过期时间。最终我们在交易结束后释放锁。延长锁的操作则有Redisson框架替我们完成,它会使用轮询去查看key是否过期,在交易没有完成时,自动重设Redis的key过期时间。1.引入依赖:<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.5</version> </dependency>2.配置redissonimport org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * redisson配置 * 目前使用的是腾讯云的单节点redis,因此暂时配置单服务 * * */ @Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port; @Value("${spring.redis.password}") private String password; @Bean public RedissonClient getRedisson(){ Config config = new Config(); config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password); //添加主从配置 //config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""}); return Redisson.create(config); } }3.示例代码import org.redisson.api.RedissonClient; import org.redisson.api.RLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BuyRedissonLock { @Autowired private RedissonClient redissonClient; @GetMapping(value = "buy") public String get() { RLock ztLock = redissonClient.getLock("ztLock"); // ztLock.lock(3, TimeUnit.SECONDS); 指定了超时时间的话,使用指定的, ztLock.lock(); //没指定的话使用30s,有看门狗,延迟10秒执行,循环延期 // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { //TODO... 业务逻辑代码 } finally { ztLock.unlock(); } return ""; } } }4、几点说明加锁的时候注意一下锁的粒度,粒度越小性能越好,比如商品的话,可以按照商品id进行锁。为了保证缓存一致性,可以使用读写锁。
2021年09月13日
396 阅读
0 评论
0 点赞
2021-09-03
Springboot整合Spring Retry实现重试机制
在项目开发过程中,经常会有这样的情况:第一次执行一个操作不成功,考虑到可能是网络原因造成,就多执行几次操作,直到得到想要的结果为止,这就是重试机制。Springboot可以通过整合Spring Retry框架实现重试。下面讲一下在之前新建的ibatis项目基础上整合Spring Retry框架的步骤:1、首先要在pom.xml配置中加入spring-retry的依赖:<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> 2、在启动类中加入重试注解@EnableRetry。import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.retry.annotation.EnableRetry; @EnableRetry //重试注解 @MapperScan("com.batis.mapper") @SpringBootApplication public class BatisApplication { public static void main(String[] args) { SpringApplication.run(BatisApplication.class, args); } } 3、新建重试接口RetryService和实现类RetryServiceImpl重试接口:public interface RetryService { void retryTransferAccounts(int fromAccountId, int toAccountId, float money) throws Exception; } 接口实现类:import com.batis.mapper.AccountMapper; import com.batis.model.Account; import com.batis.service.RetryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class RetryServiceImpl implements RetryService { @Autowired private AccountMapper accountMapper; @Transactional @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 3000, multiplier = 1, maxDelay = 10000)) @Override public void retryTransferAccounts(int fromAccountId, int toAccountId, float money) throws Exception { Account fromAccount = accountMapper.findOne(fromAccountId); fromAccount.setBalance(fromAccount.getBalance() - money); accountMapper.update(fromAccount); int a = 2 / 0; Account toAccount = accountMapper.findOne(toAccountId); toAccount.setBalance(toAccount.getBalance() + money); accountMapper.update(toAccount); throw new Exception(); } @Recover public void recover(Exception e) { System.out.println("回调方法执行!!!"); } } @Retryable:标记当前方法会使用重试机制value:重试的触发机制,当遇到Exception异常的时候,会触发重试maxAttempts:重试次数(包括第一次调用)delay:重试的间隔时间multiplier:delay时间的间隔倍数maxDelay:重试次数之间的最大时间间隔,默认为0,如果小于delay的设置,则默认为30000L@Recover:标记方法为回调方法,传参与@Retryable的value值需一致4、新建重试控制器类RetryControllerimport com.batis.service.RetryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/retry") public class RetryController { @Autowired private RetryService retryService; @RequestMapping(value = "/transfer", method = RequestMethod.GET) public String transferAccounts() { try { retryService.retryTransferAccounts(1, 2, 200); return "ok"; } catch (Exception e) { return "no"; } } } 5、启动ibatis项目进行测试,在浏览器地址栏输入:http://localhost:8080/retry/transfer可以看到,转账操作一共执行了3次,最后执行了回调方法。至此Springboot整合Spring Retry的步骤已经完成,测试也非常成功!
2021年09月03日
101 阅读
0 评论
0 点赞
2021-09-03
服务端如何防止订单重复支付?
如图是一个简化的下单流程,首先是提交订单,然后是支付。支付的话,一般是走支付网关(支付中心),然后支付中心与第三方支付渠道(微信、支付宝、银联)交互,支付成功以后,异步通知支付中心,支付中心更新自身支付订单状态,再通知业务应用,各业务再更新各自订单状态。这个过程中经常可能遇到的问题是掉单,无论是超时未收到回调通知也好,还是程序自身报错也好,总之由于各种各样的原因,没有如期收到通知并正确的处理后续逻辑等等,都会造成用户支付成功了,但是服务端这边订单状态没更新,这个时候有可能产生投诉,或者用户重复支付。由于③⑤造成的掉单称之为外部掉单,由④⑥造成的掉单我们称之为内部掉单为了防止掉单,这里可以这样处理:支付订单增加一个中间状态“支付中”,当同一个订单去支付的时候,先检查有没有状态为“支付中”的支付流水,当然支付(prepay)的时候要加个锁。支付完成以后更新支付流水状态的时候再讲其改成“支付成功”状态。支付中心这边要自己定义一个超时时间(比如:30秒),在此时间范围内如果没有收到支付成功回调,则应调用接口主动查询支付结果,比如10s、20s、30s查一次,如果在最大查询次数内没有查到结果,应做异常处理支付中心收到支付结果以后,将结果同步给业务系统,可以发MQ,也可以直接调用,直接调用的话要加重试(比如:SpringBoot Retry)无论是支付中心,还是业务应用,在接收支付结果通知时都要考虑接口幂等性,消息只处理一次,其余的忽略。业务应用也应做超时主动查询支付结果。对于上面说的超时主动查询可以在发起支付的时候将这些支付订单放到一张表中,用定时任务去扫为了防止订单重复提交,可以这样处理:1、创建订单的时候,用订单信息计算一个哈希值,判断redis中是否有key,有则不允许重复提交,没有则生成一个新key,放到redis中设置个过期时间,然后创建订单。其实就是在一段时间内不可重复相同的操作附上微信支付最佳实践:
2021年09月03日
89 阅读
0 评论
0 点赞
2021-08-16
通过输入Html提取其中的图片地址信息
/** * 通过Html获取图片链接 * @param inputHtml * @return */ public List<String> getDetailImagesListFromHtml(String inputHtml){ List<String> detailImagesList = ReUtil.findAll("url\(//(.*?(png|jpg|jpeg|webp|svg|psd|bmp|tif))", input, 1); return detailImagesList; }
2021年08月16日
135 阅读
0 评论
0 点赞
2021-07-12
Teamcity + Rancher + 阿里云Code 实现Devops 自动化部署
Teamcity + Rancher + 阿里云CODE + 阿里云镜像仓库实现DevOps1.安装DockerDocker相关知识具体参考:https://www.bdysoft.com/archives/3.html备注:由于rancher 1.6.30仅支持19.03.2一下版本建议在执行docker安装时,执行一下指令进行安装:# 1.删除服务器中原本安装的docker yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine # 2.安装yum-utils yum install -y yum-utils device-mapper-persistent-data lvm2 # 3.为yum源添加docker仓库位置 yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo # 4.yum安装docker yum -y install docker-ce-18.06.3.ce # 5.设置docker开机自启 systemctl start docker systemctl enable docker2.安装Teamcity + Teamcity Agent# 1.安装Teamcity docker run -d -u 0 -it --name teamcity -v /data/tc/server/datadir:/data/teamcity_server/datadir -p 你准备用来提供访问的接口:8111 --restart=always jetbrains/teamcity-server # 2.如果是在同一台机器部署Teamcity + Teamcity Agent,可以通过一下指令找到Teamcity的地址 docker inspect --format '{{ .NetworkSettings.IPAddress }}' teamcity-server的CONTAINERID # 3.安装Teamcity Agent docker run -d -u 0 -it -e SERVER_URL="可以访问Teamcity的地址" \ -v /data/tc/agent/conf:/data/teamcity_agent/conf \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /data/tc/agent/buildagent/work:/opt/buildagent/work \ -v /data/tc/agent/buildagent/temp:/opt/buildagent/temp \ -v /data/tc/agent/buildagent/tools:/opt/buildagent/tools \ -v /data/tc/agent/buildagent/plugins:/opt/buildagent/plugins \ -v /data/tc/agent/buildagent/system:/opt/buildagent/system \ --restart=always \ jetbrains/teamcity-agent 3.配置Teamcity 及设置Teamcity Agent到这里teamcity就配置结束了,我们尝试用temacity打包并推送到阿里云镜像仓库。4.利用Teamcity 完成项目创建及配置项目中必须包含Dockerfile文件,Dockerfile文件里面的一些参数可参考以下说明,形象的理解。备注:测试所用的阿里云code,输入账号密码登录有问题,所以需要在project上传SSHkey 。5.配置阿里云镜像仓库与Teamcity关联备注:所需要的密码是在这里设置的:6.配置Teamcity 发版流水线先设置maven clean package构建镜像推送镜像, 其中%name%是在parameters里面配置的:推送完成, 其中%name%是在parameters里面配置的:6.在一台【Docker环境干净的】服务器上创建Rancher 1.6.30版本1.具体安装教程:https://docs.rancher.cn/docs/rancher1/overview/start/_index/2.注意需要开放以下接口:https://www.cnblogs.com/heqiuyong/p/10460150.htmlhttp://docs.rancher.cn/docs/rancher2/installation/requirements/ports/_index3.开放端口firewall-cmd --zone=public --add-port=6443/tcp --permanent # 开放5672端口firewall-cmd --zone=public --remove-port=6443/tcp --permanent #关闭5672端口firewall-cmd --reload # 配置立即生效4.查看防火墙所有开放的端口firewall-cmd --zone=public --list-ports# 1.安装前可以清理一下容器: docker stop $(docker ps -aq) docker system prune -f docker volume rm $(docker volume ls -q) docker image rm $(docker image ls -q) rm -rf /etc/ceph \ /etc/cni \ /etc/kubernetes \ /opt/cni \ /opt/rke \ /run/secrets/kubernetes.io \ /run/calico \ /run/flannel \ /var/lib/calico \ /var/lib/etcd \ /var/lib/cni \ /var/lib/kubelet \ /var/lib/rancher/rke/log \ /var/log/containers \ /var/log/pods \ /var/run/calico # 2.安装Docker 镜像 docker run -d --restart=unless-stopped -p 80:8080 rancher/server:stable7.完成Rancher 主机注册并配置镜像仓库:1.添加镜像仓库2.注册主机,选择custom,将主机注册指令,复制到需要注册的主机节点上即可#可通过修改主机名: hostnamectl set-hostname node2 8.创建应用9.创建服务10.通过触发器实现镜像一旦在阿里云仓库更新则容器上的服务自动更新11.通过Rancher API 实现在Teamcity里通过输入build的版本号部署对应的服务前置条件为每次build及push到阿里云镜像仓库的版本号都不一样。1.打开浏览器的开发者模式,点击服务右侧的升级按钮,复制浏览器发送请求的升级参数,然后API查看里面打开,找到upgrade那个API,将参数复制进去后show requerst,复制完整的requert,并且将其中appkey 和 secret 换成自己生成的,还有其中有一个参数是imageUuid,我们可以在imageUUid对应的镜像末尾修改为%version%,这个version就是前面我们设计的必须要输入的参数。2.在teamcity中添加build step,选择runner type = Command Line,在下面的命令行输入上面复制并进行修改的代码即可。
2021年07月12日
475 阅读
2 评论
0 点赞
2021-04-07
分享一个我正在使用的CookieUtils工具类
CookieUtils包含功能得到Cookie的值,是否解编码设置Cookie的值 不设置生效时间默认浏览器关闭即失效设置Cookie的值 在指定时间内生效删除Cookie带cookie域名设置Cookie的值,并使其在指定时间内生效得到cookie的域名判断是否是一个IP代码如下:import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; public final class CookieUtils { final static Logger logger = LoggerFactory.getLogger(CookieUtils.class); /** * * @Description: 得到Cookie的值, 不编码 * @param request * @param cookieName * @return */ public static String getCookieValue(HttpServletRequest request, String cookieName) { return getCookieValue(request, cookieName, false); } /** * * @Description: 得到Cookie的值 * @param request * @param cookieName * @param isDecoder * @return */ public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) { Cookie[] cookieList = request.getCookies(); if (cookieList == null || cookieName == null) { return null; } String retValue = null; try { for (int i = 0; i < cookieList.length; i++) { if (cookieList[i].getName().equals(cookieName)) { if (isDecoder) { retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8"); } else { retValue = cookieList[i].getValue(); } break; } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return retValue; } /** * * @Description: 得到Cookie的值 * @param request * @param cookieName * @param encodeString * @return */ public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) { Cookie[] cookieList = request.getCookies(); if (cookieList == null || cookieName == null) { return null; } String retValue = null; try { for (int i = 0; i < cookieList.length; i++) { if (cookieList[i].getName().equals(cookieName)) { retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString); break; } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return retValue; } /** * * @Description: 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码 * @param request * @param response * @param cookieName * @param cookieValue */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue) { setCookie(request, response, cookieName, cookieValue, -1); } /** * * @Description: 设置Cookie的值 在指定时间内生效,但不编码 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage) { setCookie(request, response, cookieName, cookieValue, cookieMaxage, false); } /** * * @Description: 设置Cookie的值 不设置生效时间,但编码 * 在服务器被创建,返回给客户端,并且保存客户端 * 如果设置了SETMAXAGE(int seconds),会把cookie保存在客户端的硬盘中 * 如果没有设置,会默认把cookie保存在浏览器的内存中 * 一旦设置setPath():只能通过设置的路径才能获取到当前的cookie信息 * @param request * @param response * @param cookieName * @param cookieValue * @param isEncode */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, boolean isEncode) { setCookie(request, response, cookieName, cookieValue, -1, isEncode); } /** * * @Description: 设置Cookie的值 在指定时间内生效, 编码参数 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage * @param isEncode */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) { doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode); } /** * * @Description: 设置Cookie的值 在指定时间内生效, 编码参数(指定编码) * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage * @param encodeString */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) { doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString); } /** * * @Description: 删除Cookie带cookie域名 * @param request * @param response * @param cookieName */ public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) { doSetCookie(request, response, cookieName, null, -1, false); // doSetCookie(request, response, cookieName, "", -1, false); } /** * * @Description: 设置Cookie的值,并使其在指定时间内生效 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage cookie生效的最大秒数 * @param isEncode */ private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) { try { if (cookieValue == null) { cookieValue = ""; } else if (isEncode) { cookieValue = URLEncoder.encode(cookieValue, "utf-8"); } Cookie cookie = new Cookie(cookieName, cookieValue); if (cookieMaxage > 0) cookie.setMaxAge(cookieMaxage); if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); logger.info("========== domainName: {} ==========", domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName); } } cookie.setPath("/"); response.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } } /** * * @Description: 设置Cookie的值,并使其在指定时间内生效 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage cookie生效的最大秒数 * @param encodeString */ private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) { try { if (cookieValue == null) { cookieValue = ""; } else { cookieValue = URLEncoder.encode(cookieValue, encodeString); } Cookie cookie = new Cookie(cookieName, cookieValue); if (cookieMaxage > 0) cookie.setMaxAge(cookieMaxage); if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); logger.info("========== domainName: {} ==========", domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName); } } cookie.setPath("/"); response.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } } /** * * @Description: 得到cookie的域名 * @return */ private static final String getDomainName(HttpServletRequest request) { String domainName = null; String serverName = request.getRequestURL().toString(); if (serverName == null || serverName.equals("")) { domainName = ""; } else { serverName = serverName.toLowerCase(); serverName = serverName.substring(7); final int end = serverName.indexOf("/"); serverName = serverName.substring(0, end); if (serverName.indexOf(":") > 0) { String[] ary = serverName.split("\\:"); serverName = ary[0]; } final String[] domains = serverName.split("\\."); int len = domains.length; if (len > 3 && !isIp(serverName)) { // www.xxx.com.cn domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1]; } else if (len <= 3 && len > 1) { // xxx.com or xxx.cn domainName = "." + domains[len - 2] + "." + domains[len - 1]; } else { domainName = serverName; } } return domainName; } public static String trimSpaces(String IP){//去掉IP字符串前后所有的空格 while(IP.startsWith(" ")){ IP= IP.substring(1,IP.length()).trim(); } while(IP.endsWith(" ")){ IP= IP.substring(0,IP.length()-1).trim(); } return IP; } public static boolean isIp(String IP){//判断是否是一个IP boolean b = false; IP = trimSpaces(IP); if(IP.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){ String s[] = IP.split("\\."); if(Integer.parseInt(s[0])<255) if(Integer.parseInt(s[1])<255) if(Integer.parseInt(s[2])<255) if(Integer.parseInt(s[3])<255) b = true; } return b; } }
2021年04月07日
118 阅读
0 评论
0 点赞
2021-03-17
支付宝支付前后端实现(Vue+Spring Boot)
本文主要总结基于Vue/Spring Boot的支付宝支付实现,兼容H5与电脑端。1、应用创建与配置第一步:登录支付宝开放平台创建应用; 并视情况需要添加“手机网站支付”和“电脑网站支付”能力;手机网站支付可通过浏览器打开支付宝应用程序进行付款,但在微信公众号打开的页面无法通过支付宝进行支付,电脑端的可通过二维码或者登录支付宝账号付款。第二步:密钥配置包括应用私钥、应用公钥和支付宝公钥。可通过支付宝开放平台开发助手生成应用私钥与应用公钥,然后在开放平台中将应用公钥配置到应用中,生成支付宝公钥。最终程序中需要使用到应用私钥与支付宝公钥,其中应用私钥用于对发往支付宝的消息进行加密,而支付宝公钥用于对支付宝返回的数据进行验签。具体配置过程如下:使用支付宝开放平台助手生成应用私钥与公钥切记生成后将私钥与公钥另外保存;登录开放平台配置应用公钥点击接口加签方式中的设置按钮,在弹出窗口中将应用公钥复制进去,然后生成支付宝公钥,将公钥复制下来保存,后续在代码中会需要使用到。生成支付宝公钥后应用公钥基本就用不着了,后续的代码主要使用的是应用私钥和支付宝公钥。2、处理流程概述支付处理步骤如下所示:3、业务的具体实现3.1 前端发起具体支付界面视业务场景而定,用户选择或填写相关信息后,点击确定按钮时调用后端接口生成订单编号,然后将相关信息展示给用户确认,用户确认后再发起付款流程。发起付款流程前端关键代码如下所示:this.$post("/pay/refill", { orderNo: this.orderNo, fee: this.selectedPlan.refillMoney, channel: this.refillChannel === "wx" ? 2 : 1, tbAmount: this.selectedPlan.payMoney, tbSentAmount: this.isVip ? this.selectedPlan.payMoney * 0.5 : 0, }).then((resp) => { this.refillCompleted = true; if (this.refillChannel === "wx") { this.$goPath("/user/pay/wx-pay", resp); } else { // 支付宝 // 打开单独的支付宝页面 this.$goPath("/user/pay/ali-pay", resp); } });注意我的项目支持微信与支付宝两种支付方式,因此前端选择完后会将所选择的方式、金额等信息传给后台生成支付表单3.2 支付表单生成1.依赖包下载:后端需要使用到阿里的包,maven地址:<dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.10.111.ALL</version> </dependency>2.订单生成前端发起订单生成请求后,后端生成订单记录,并根据微信还是支付宝来判断调用微信还是支付宝接口完成支付表单生成。/** * 充值 * * @param refill 充值 * @return 支付相关信息 */ @PreAuthorize("isAuthenticated()") @PostMapping("/refill") public PayDTO refill(@RequestBody @Validated RefillDTO refill) { UserDTO user = this.getLoginUserOrThrow(); if (null == tradeService.findByNo(refill.getOrderNo())) { // 保存订单 TradeDTO tradeDTO = new TradeDTO(); ... tradeService.save(user, tradeDTO); } // 获取支付表单信息 if (refill.getChannel().equals(TradeChannel.WX)) { // 微信 String openId = user.getWxOpenId(); if (refill.getPlatform().equals(1)) { openId = user.getMpOpenId(); } return wxPayService.getPayUrl(openId, refill.getOrderNo(), refill.getFee(), TradeType.REFILL, refill.getPlatform()); } else if (refill.getChannel().equals(TradeChannel.ALIPAY)) { // 支付宝 return aliPayService.getPayForm(refill.getOrderNo(), refill.getFee(), TradeType.REFILL, refill.getPlatform()); } else { throw new UnsupportedOperationException("不支持的支付渠道"); } }3.支付表单生成/** * 获取支付表单 * * @param orderNo 订单号 * @param fee 订单金额 * @param tradeType 交易类型 * @param platform 平台,0:WEB;1:移动端 * @return 下单表单 */ public PayDTO getPayForm(String orderNo, Double fee, TradeType tradeType, Integer platform) { boolean isH5 = Optional.ofNullable(platform).orElse(0).equals(1); AlipayClient alipayClient = new DefaultAlipayClient(API_URL, APP_ID, APP_KEY, "json", "utf-8", ZFB_PUBLIC_KEY, SIGN_TYPE); Map<String, String> params = MapEnhancer.<String, String>create() .put("out_trade_no", orderNo) .put("total_amount", String.format("%.2f", fee)) .put("subject", tradeType.getName()) .put("product_code", "FAST_INSTANT_TRADE_PAY") .get(); String body; try { if (isH5) { if (logger.isDebugEnabled()) { logger.debug("return url: {}", h5ReturnUrl); } AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); request.setBizContent(JSONObject.toJSONString(params)); request.setReturnUrl(h5ReturnUrl); request.setNotifyUrl(notifyUrl); body = alipayClient.pageExecute(request).getBody(); } else { AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); request.setBizContent(JSONObject.toJSONString(params)); request.setReturnUrl(returnUrl); request.setNotifyUrl(notifyUrl); body = alipayClient.pageExecute(request).getBody(); } } catch (AlipayApiException e) { logger.error("生成支付宝支付表单失败", e); throw BusinessException.create("生成支付宝支付表单失败"); } if (logger.isDebugEnabled()) { logger.debug("支付表单内容: {}", body); } PayDTO payDTO = new PayDTO(); payDTO.setFee(fee); payDTO.setOrderNo(orderNo); payDTO.setCodeUrl(body); return payDTO; }其中API_URL为支付宝网关地址(https://openapi.alipay.com/gateway.do),APP_ID为支付宝开放平台中的应用id, APP_KEY为前面密钥配置中的应用私钥;ZFB_PUBLIC_KEY为密钥配置中的支付宝公钥。注意需要根据h5及pc网站类型来判断使用哪个API;API关键参数如下:调用接口成功后将请求返回的数据与订单号、交易金额一起返回给前端,由前端进行下一步处理。另外在调用接口时,还需要指定returnUrl与notifyUrl,其中returnUrl是支付宝在用户支付完成后跳转到的页面;notifyUrl是用于接收支付宝支付结果的地址(一般为后端地址,由支付宝调用)。3.3 前端跳转处理前端接收到后端生成的支付表单后,跳转到一个专门的页面,将返回的内容加载到页面中,并自动触发表单提交,处理如下:(ali-pay.vue)<template> <!-- 支付宝支付界面 --> <div class="ali-pay box-shadow mt-4 border-radius"> <div v-html="payInfo.codeUrl" ref="pay"></div> </div> </template> <script> export default { components: {}, props: [], data() { return { payInfo: {}, }; }, mounted() { this.payInfo = this.$route.query; this.$nextTick(() => { this.$refs.pay.children[0].submit(); }); }, methods: {}, }; </script> <style lang="scss"> .ali-pay { } </style>经过以上处理,h5端将会调起支付宝软件进行支付,web端则会显示二维码或账号输入页面。3.4 支付返回页面处理及支付状态更新用户支付完成后,支付宝将会返回生成表单时指定的returnUrl地址,同时会附带订单号在查询参数中;在这个页面中我们可以根据订单号,通过后台调用支付宝接口查询订单状态,然后依此来更新系统订单支付状态。前端页面如下(待完善,可以在trade/status接口返回查询的订单状态,在页面做相应提示):<template> <!-- 支付宝支付界面 --> <div class="ali-pay-success box-shadow mt-4 border-radius p-4"> <div> 支付成功,您可以进入 <el-button type="text" @click="$goPath('/user/account/refill-history')" >充值记录</el-button >查看历史记录 </div> </div> </template> <script> export default { components: {}, props: [], data() { return { payInfo: {}, }; }, mounted() { this.payInfo = this.$route.query; // 更新支付状态 this.$get("/trade/status", { no: this.payInfo.out_trade_no }); }, methods: {}, }; </script><style lang="scss"> .ali-pay-success { } </style>后端/trade/status接口实现如下:/** * 查询交易状态 * * @param no 交易编号 * @return 交易状态,0:失败,1:成功,2:未知 */ @GetMapping("/status") public Integer getTradeStatus(@RequestParam("no") String no) { TradeDTO trade = tradeService.findByNo(no); if (null == trade) { throw BusinessException.create("订单不存在"); } if (trade.getState() == 1 || trade.getState() == 0) { return trade.getState(); } // 交易状态未知时,通过相关支付服务查询状态 if (trade.getChannel().equals(TradeChannel.WX)) { return wxPayService.getTradeStatus(trade); } else if (trade.getChannel().equals(TradeChannel.ALIPAY)) { return aliPayService.getTradeStatus(trade); } return trade.getState(); }这里先在系统里面查询订单状态(状态可能已经变更,因为我们同时会接收支付宝支付成功的通知),如果订单状态未知,那么通过支付宝或微信支付的接口更新订单状态。支付宝支付状态查询接口实现:/** * 获取订单状态 * * @param trade 订单信息 * @return 订单状态 */ public Integer getTradeStatus(TradeDTO trade) { AlipayClient alipayClient = new DefaultAlipayClient(API_URL, APP_ID, APP_KEY, "json", "utf-8", ZFB_PUBLIC_KEY, SIGN_TYPE); AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); Map<String, String> params = MapEnhancer.<String, String>create() .put("out_trade_no", trade.getNo()) .get(); request.setBizContent(JSONObject.toJSONString(params)); AlipayTradeQueryResponse response; try { response = alipayClient.execute(request); } catch (AlipayApiException e) { logger.error("查询订单状态失败", e); return trade.getState(); } if (logger.isDebugEnabled()) { logger.debug("订单信息: {}", JSONObject.toJSONString(response)); } if (response.isSuccess()) { String tradeStatus = response.getTradeStatus(); if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) { tradeService.tradeSuccess(trade); } else if (tradeStatus.equals("TRADE_CLOSED")) { tradeService.tradeFailed(trade); } } return trade.getState(); }注意查询到结果后,需要更新交易状态,并视情况进行下一步的业务处理,如用户充值的就得增加用户余额等。3.5 支付结果接收至此支付过程基本已经完成。但某些情况用户支付完成后并未返回我们指定的页面,如用户可能直接关闭浏览器等极端情况,因此必须与支付宝推送结果的方式结合,来防止状态不一致。接收支付宝支付结果的接口实现如下:@RequestMapping("ali-notify") public String aliNotify(@RequestParam Map<String, String> params) { if (logger.isDebugEnabled()) { logger.debug("收到支付宝通知: {}", params); } return aliPayService.parseAndSaveTradeResult(params); }具体service方法实现如下:/** * 解析通知结果并保存 * * @param params 通知结果 */ public String parseAndSaveTradeResult(Map<String, String> params) { if (logger.isDebugEnabled()) { logger.debug("接收到支付宝支付结果: {}", params); } // 结果验签 boolean signVerified; try { signVerified = AlipaySignature.rsaCheckV1(params, ZFB_PUBLIC_KEY, "utf-8", SIGN_TYPE); } catch (AlipayApiException e) { logger.error("验签失败", e); return "failure"; } if (!signVerified) { logger.warn("验签失败,通知内容:{}", params); return "failure"; } String orderNo = MapUtils.getString(params, "out_trade_no", ""); if (StringUtils.isBlank(orderNo)) { logger.warn("订单号为空,通知内容:{}", params); return "success"; } // 根据订单号查询订单 TradeDTO trade = tradeService.findByNo(orderNo); if (null == trade) { logger.warn("订单不存在,通知内容:{}", params); return "success"; } if (trade.getState() == 1) { logger.debug("订单已成功"); return "success"; } String tradeStatus = MapUtils.getString(params, "trade_status", ""); if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) { trade.setState(1); tradeService.tradeSuccess(trade); } else { trade.setState(0); tradeService.tradeFailed(trade); } return "success"; }注意如果查询到支付状态是成功的,就需要进行下一步的业务处理了,如增加用户余额等。实际交易成功处理,因为同一笔订单可能会有两个进程更新交易状态(通道消息和主动查询的消息),这两个操作肯定只能有一个操作能够生效;如果是单节点情况,可以通过JVM锁来控制;如果多节点的,就需要通过分页式锁来控制了。我的项目里面是采用了Redis实现了个简单的分布式锁处理,具体实现方案网上一大堆,不在此展开。另外,可以看到我的代码里面有很多关于微信支付与支付宝支付的判断,这是因为我同时支持了微信支付与支付宝支付;这两者的支付过程基本类似,本文主要讲的是支付宝支付,微信的未展开,后续再单独对微信支付实现做一个补充。
2021年03月17日
258 阅读
0 评论
0 点赞
2021-03-14
JAVA秒杀系统的简单实现(Redis+RabbitMQ)
1、分析秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。秒杀业务流程比较简单,一般就是下订单减库存。上述三点的主要问题就是在高并发的情况下保证数据的一致性。2、使用的技术和架构2.1 秒杀架构图2.2 实现流程使用 redis 缓存秒杀的商品信息,秒杀成功后使用消息队列发送订单信息,然后将更新后数据重新写入redis。RabbitMQ监听器在接受到消息后,将订单信息写入数据库。在秒杀时使用redisson对商品信息上锁。2.3 流程图3、准备工作3.1 安装redis cluster教程一大堆,这里我就不多赘述了,可以参考:https://blog.csdn.net/CFrieman/article/details/835830853.2 安装RabbitMQ和erlang教程一大堆,这里我就不多赘述了,可以参考:https://blog.csdn.net/qq_36505948/article/details/827341334、具体实现4.1 SeckillServicepublic class SeckillService { @Autowired private RedisClusterClient rt; @Autowired private SeckillMapper sm; @Autowired private RedissonClient redissonClient; // 加锁 @Autowired private RabbitmqSendMessage rsm; @Autowired private SecorderMapper om; /** * 初始化 ,将mysql中的商品信息缓存到redis中 * @return */ public List<Seckill> querySeckill() { List<Seckill> list = (List<Seckill>) rt.get("secgoods"); if(list==null) { list = sm.selectByExample(null); rt.set("secgoods", list, 60*30); } return list; } public boolean queryStartTime(Seckill sec) { Date date = new Date();// 比较时间,是否到秒杀时间 Date startTime = sec.getStarttime(); // 秒杀活动还未开始 if (startTime.getTime() > date.getTime()) { return false; } return true; } // 减库存redis public void decreaseStock(String id) { int goodsid = Integer.parseInt(id); List<Seckill> list = (List<Seckill>) rt.get("secgoods"); if (list!=null) { for (Seckill sec : list) { if (goodsid==sec.getId()) { sec.setCount(sec.getCount()-1); //写回redis rt.set("secgoods", list, 60*30); return ; } } } } // public Seckill findSec(String secid) { List<Seckill> list = (List<Seckill>) rt.get("secgoods"); int id = Integer.parseInt(secid); for(Seckill sec:list) { if(sec.getId()==id) { return sec; } } return null; } // 开始秒杀 public String goSeckill(String goodsid, String username) { String key = username + ":" + goodsid; String secid = goodsid; Long value = (Long) rt.get(key); if (value != null) { return "exist"; } Seckill sec = findSec(secid); boolean flag = queryStartTime(sec); if (!flag) { return "notTime"; } RLock rLock = redissonClient.getLock("miaosha"); rLock.lock(); if (sec.getCount() > 0) { decreaseStock(goodsid); // 减少库存 rt.set(key, System.currentTimeMillis(), 60*30); Secorder newOrder = new Secorder(); newOrder.setCreatetime(new Date()); newOrder.setGoodsid(Integer.parseInt(goodsid)); newOrder.setStatus("未付款"); newOrder.setUsername(username); String json = JSONObject.toJSONString(newOrder); rsm.send(json); // 异步下单 rLock.unlock(); // 解锁 return "success"; } else { rLock.unlock(); return "failed"; } } // 写入mysql public void saveOrder(String json) { Secorder order = JSON.parseObject(json, Secorder.class); int n = sm.updateCount(order.getGoodsid()); int m = om.insert(order); } }4.2 RabbitmqListenner@Service public class RabbitmqListenner implements MessageListener { @Autowired private SeckillService ss; @Override public void onMessage(Message msg) { byte[] data = msg.getBody(); try { String json = new String(data,"utf-8"); System.out.println(json); ss.saveOrder(json); //将监听到的订单写入MySQL } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }4.3 RabbitmqSendMessagepublic class RabbitmqSendMessage { @Autowired private RabbitTemplate rt; private final String QUEEN_NAME = "MIAOSHA"; /** * 发送消息 * @param msg */ public void send(String msg) { rt.convertAndSend(QUEEN_NAME,msg); } }4.4以上就是整个业务流程的核心代码,使用redisson保证数据一致性,用rabbitmq异步下单将下单及写数据库这个长操作变成两个短操作。GitHub源码地址,关于数据库建表什么的,大家直接去源码里看吧。5.优化限流:使用验证码,请求秒杀接口需要验证图形验证码的正确性,这样也很好的防止脚本的不断访问;防刷:一个用户对一个路径的访问次数在一定时间内有限制,使用redis可以解决接口地址隐藏:接口地址传参,保证秒杀接口不是一个固定路径,防止接口被刷,同时也可以有效隐藏秒杀地址。
2021年03月14日
312 阅读
0 评论
1 点赞
2021-03-12
Docker Compose的介绍安装与使用
一、Docker ComposeCompose 简介Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。Compose 使用的三个步骤:使用 Dockerfile 定义应用程序的环境。使用 docker-compose.yml 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。最后,执行 docker-compose up 命令来启动并运行整个应用程序。docker-compose.yml 的配置案例如下(配置参数参考下文):实例:# yaml 配置实例 version: '3' services: web: build: . ports: - "5000:5000" volumes: - .:/code - logvolume01:/var/log links: - redis redis: image: redis volumes: logvolume01: {}Compose 安装LinuxLinux 上我们可以从 Github 上下载它的二进制包来使用,最新发行的版本地址:https://github.com/docker/compose/releases。运行以下命令以下载 Docker Compose 的当前稳定版本:$ sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose{message type="info"}要安装其他版本的 Compose,请替换 1.24.1。{/message}将可执行权限应用于二进制文件:$ sudo chmod +x /usr/local/bin/docker-compose创建软链:$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose测试是否安装成功:$ docker-compose --version{message type="info"}注意: 对于 alpine,需要以下依赖包: py-pip,python-dev,libffi-dev,openssl-dev,gcc,libc-dev,和 make。{/message}二、Docker Compose使用2.1.准备工作2.1.1 创建一个测试目录:$ mkdir composetest $ cd composetest2.1.2 在测试目录中创建一个名为 app.py 的文件,并复制粘贴以下内容:import time import redis from flask import Flask app = Flask(__name__) cache = redis.Redis(host='redis', port=6379) def get_hit_count(): retries = 5 while True: try: return cache.incr('hits') except redis.exceptions.ConnectionError as exc: if retries == 0: raise exc retries -= 1 time.sleep(0.5) @app.route('/') def hello(): count = get_hit_count() return 'Hello World! I have been seen {} times.\n'.format(count){message type="info"}在此示例中,redis 是应用程序网络上的 redis 容器的主机名,该主机使用的端口为 6379。{/message}2.1.3在 composetest 目录中创建另一个名为 requirements.txt 的文件,内容如下:flask redis2.2 创建 Dockerfile 文件2.2.1 在 composetest 目录中,创建一个名为的文件 Dockerfile,内容如下:FROM python:3.7-alpine WORKDIR /code ENV FLASK_APP app.py ENV FLASK_RUN_HOST 0.0.0.0 RUN apk add --no-cache gcc musl-dev linux-headers COPY requirements.txt requirements.txt RUN pip install -r requirements.txt COPY . . CMD ["flask", "run"]2.2.2 Dockerfile 内容解释:①:FROM python:3.7-alpine: 从 Python 3.7 映像开始构建镜像。②:WORKDIR /code: 将工作目录设置为 /code。③:ENV FLASK_APP app.py ENV FLASK_RUN_HOST 0.0.0.0 设置 flask 命令使用的环境变量。④:RUN apk add --no-cache gcc musl-dev linux-headers: 安装 gcc,以便诸如 MarkupSafe 和 SQLAlchemy 之类的 Python 包可以编译加速。⑤:COPY requirements.txt requirements.txt RUN pip install -r requirements.txt 复制 requirements.txt 并安装 Python 依赖项。⑥:COPY . .: 将 . 项目中的当前目录复制到 . 镜像中的工作目录。⑦:CMD ["flask", "run"]: 容器提供默认的执行命令为:flask run。2.3 创建 docker-compose.yml2.3.1 在测试目录中创建一个名为 docker-compose.yml 的文件,然后粘贴以下内容:# yaml 配置 version: '3' services: web: build: . ports: - "5000:5000" redis: image: "redis:alpine"该 Compose 文件定义了两个服务:web 和 redis。①:web:该 web 服务使用从 Dockerfile 当前目录中构建的镜像。然后,它将容器和主机绑定到暴露的端口 5000。此示例服务使用 Flask Web 服务器的默认端口 5000 。②:redis:该 redis 服务使用 Docker Hub 的公共 Redis 映像。2.4 使用 Compose 命令构建和运行您的应用2.4.1 在测试目录中,执行以下命令来启动应用程序:docker-compose up2.4.2 如果你想在后台执行该服务可以加上 -d 参数:docker-compose up -d
2021年03月12日
104 阅读
0 评论
0 点赞
2021-03-12
SpringBoot+RabbitMQ实现手动Consumer Ack
一、Consumer Ack的三种方式自动确认:acknowledge = “none”,这是默认的方式,如果不配置的话,默认就是自动确认,消费方从消息队列中拿出消息后,消息队列中都会清除掉这条消息(不安全).手动确认:acknowledge = “manual”,手动确认就是当消费者取出来消息其后的操作正常执行后,返回给消息队列,让其清除该条消息;如果后续执行有异常,可以设置requeue=true返回其消息队列,再让其消息队列重新给消费者发送消息.根据异常情况确认(很麻烦):acknowledge = “auto”.二、SpringBoot+RabbitMQ实现手动Consumer Ack1.pom文件中导入依赖坐标<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>2.在生产者与消费者工程yml配置文件中开启手动Ackspring: rabbitmq: host: 192.168.253.128 #ip username: guest password: guest virtual-host: / port: 5672 listener: simple: acknowledge-mode: manual #开启手动Ack3.在生产者工程中创建一个配置类声明队列与交换机的关系import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQConfig { //交换机的名称; public static final String DIRECT_EXCHANGE_NAME = "direct_boot_exchange"; //队列名称; public static final String DIRECT_QUEUE_NAME = "direct_boot_queue"; /** * 声明交换机,在以后我们会定义多个交换机, * 所以给这个注入的Bean起一个名字,同理在绑定的时候用@Qualifier注解; * durablie:持久化 */ @Bean("directExchange") public Exchange directExchange(){ return ExchangeBuilder.directExchange(DIRECT_EXCHANGE_NAME).durable(true).build(); } //声明队列; @Bean("directQueue") public Queue testQueue(){ return QueueBuilder.durable(DIRECT_QUEUE_NAME).build(); } //绑定交换机和队列,把上述声明的交换机、队列作为参数传入进来; @Bean public Binding bindDirectExchangeQueue(@Qualifier("directQueue") Queue queue, @Qualifier("directExchange") Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with("info").noargs(); } } 4.在消费者工程中创建一个组件监听在生产者声明的队列import com.rabbitmq.client.Channel; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.handler.annotation.Header; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class MyAckListener { /** * * @param message 队列中的消息; * @param channel 当前的消息队列; * @param tag 取出来当前消息在队列中的的索引, * 用这个@Header(AmqpHeaders.DELIVERY_TAG)注解可以拿到; * @throws IOException */ @RabbitListener(queues = "direct_boot_queue") public void myAckListener(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException { System.out.println(message); try { /** * 无异常就确认消息 * basicAck(long deliveryTag, boolean multiple) * deliveryTag:取出来当前消息在队列中的的索引; * multiple:为true的话就是批量确认,如果当前deliveryTag为5,那么就会确认 * deliveryTag为5及其以下的消息;一般设置为false */ channel.basicAck(tag, false); }catch (Exception e){ /** * 有异常就绝收消息 * basicNack(long deliveryTag, boolean multiple, boolean requeue) * requeue:true为将消息重返当前消息队列,还可以重新发送给消费者; * false:将消息丢弃 */ channel.basicNack(tag,false,true); } } }5.在生产者中创建一个测试类来发送消息import com.itlw.config.RabbitMQConfig; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class ProducedTest { //从IOC容器中拿模板类; @Autowired private RabbitTemplate rabbitTemplate; @Test public void test(){ //发送消息; rabbitTemplate.convertAndSend(RabbitMQConfig.DIRECT_EXCHANGE_NAME, "info","这是一条测试消息...."); } } 6.启动消费者工程来接收此队列的消息可以看到控制台输出了接收到的消息,并且因为已经被确认,所以队列中消息已经为0,要测出效果,手动添加一个异常.7.手动添加一个异常try { /** * 无异常就确认消息 * basicAck(long deliveryTag, boolean multiple) * deliveryTag:取出来当前消息在队列中的的索引; * multiple:为true的话就是批量确认,如果当前deliveryTag为5,那么就会确认 * deliveryTag为5及其以下的消息;一般设置为false */ int i = 3 / 0;//手动添加异常 channel.basicAck(tag, false); } catch (Exception e) { /** * 有异常就绝收消息 * basicNack(long deliveryTag, boolean multiple, boolean requeue) * requeue:true为将消息重返当前消息队列,还可以重新发送给消费者; * false:将消息丢弃 */ channel.basicNack(tag, false, true); }8.再次运行看结果我设置了 channel.basicNack(tag, false, true);第三个requeue属性为true由队列又重新发送给消费者,消费者接收到消息后确认之前遇到了错误又重新拒收消息…所以进入了一个死循环等暂停运行后,可以看到消息队列中还剩一条消息,就是消费者绝收的这条消息,如果把requeue设置为false,那么这个队列中将没有这条消息.
2021年03月12日
236 阅读
0 评论
1 点赞
2021-03-11
给Typecho文章页添加一个赞赏功能
1.添加JS代码在post.php 文件类的 <?php $this->need('public/article.php'); ?> 这一行下面添加以下代码:<div style="padding: 10px 0; margin: 20px auto; width: 100%; font-size:16px; text-align: center;"> <button id="rewardButton" disable="enable" onclick="var qr = document.getElementById('QR'); if (qr.style.display === 'none') {qr.style.display='block';} else {qr.style.display='none'}"> <span>打赏</span> </button> <div id="QR" style="display: none;"> <div id="wechat" style="display: inline-block"> <a class="fancybox" rel="group"><img id="wechat_qr" src="https://oss.bdysoft.com/zhifbaodashang.png" alt="WeChat Pay"></a> <p> 微信打赏 </p> </div> <div id="alipay" style="display: inline-block"> <a class="fancybox" rel="group"><img id="alipay_qr" src="https://oss.bdysoft.com/zhifubaodashangma.png" alt="Alipay"></a> <p> 支付宝打赏 </p> </div> </div> </div>2.添加CSS代码在管理后台-> 全局设置-> 自定义CSS(非必填) 里面添加以下CSS代码#QR{padding-top:20px;} #QR a{border:0} #QR img{width:180px;max-width:100%;display:inline-block;margin:.8em 2em 0 2em} #rewardButton{border:1px solid #ccc;line-height:36px;text-align:center;height:36px;display:block;border-radius:4px;-webkit-transition-duration:.4s;transition-duration:.4s;background-color:#fff;color:#999;margin:0 auto;padding:0 25px} #rewardButton:hover{color:#f77b83;border-color:#f77b83;outline-style:none} #rewardButton{background-color: #f05050;color: white;border-radius: 50px;cursor: pointer;}
2021年03月11日
144 阅读
0 评论
1 点赞
2021-03-10
除了负载均衡,Nginx 能做的真是太强大了!
Nginx应该是现在最火的web和反向代理服务器,没有之一。她是一款诞生于俄罗斯的高性能web服务器,尤其在高并发情况下,相较Apache,有优异的表现。那除了负载均衡,她还有什么其他的用途呢,下面我们来看下。一、静态代理Nginx擅长处理静态文件,是非常好的图片、文件服务器。把所有的静态资源的放到nginx上,可以使应用动静分离,性能更好。二、负载均衡Nginx通过反向代理可以实现服务的负载均衡,避免了服务器单节点故障,把请求按照一定的策略转发到不同的服务器上,达到负载的效果。常用的负载均衡策略有,1、轮询将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。2、加权轮询不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。3、ip_hash(源地址哈希法)根据获取客户端的IP地址,通过哈希函数计算得到一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客户端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。4、随机通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。5、least_conn(最小连接数法)由于后端服务器的配置不尽相同,对于请求的处理有快有慢,最小连接数法根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。三、限流Nginx的限流模块,是基于漏桶算法实现的,在高并发的场景下非常实用。1、配置参数1)limit_req_zone定义在http块中,$binary_remote_addr 表示保存客户端IP地址的二进制形式。2)Zone定义IP状态及URL访问频率的共享内存区域。zone=keyword标识区域的名字,以及冒号后面跟区域大小。16000个IP地址的状态信息约1MB,所以示例中区域可以存储160000个IP地址。3)Rate定义最大请求速率。示例中速率不能超过每秒100个请求。2、设置限流burst排队大小,nodelay不限制单个请求间的时间。四、缓存1、浏览器缓存,静态资源缓存用expire。2、代理层缓存五、黑白名单1、不限流白名单2、黑名单
2021年03月10日
112 阅读
0 评论
0 点赞
2021-03-10
Java中的微信支付(2):API V3 微信平台证书的获取与刷新
1. 前言在Java 中的微信支付(1):API V3 版本签名详解一文中胖哥讲解了微信支付 V3 版本 API 的签名,当我方(你自己的服务器)请求微信支付服务器时需要根据我方的API 证书对参数进行加签,微信服务器会根据我方签名验签以确定请求来自我方服务器。那么同样的道理我方的服务器也要对微信支付服务器的响应进行鉴别来确定响应真的来自微信支付服务器,这就是验签。验签使用的是【微信支付平台证书公钥】,不是商户 API 证书。使用商户 API 证书是验证不过的。今天就来分享一下如何获得微信平台公钥和动态刷新微信平台公钥。2. 获取微信平台证书公钥微信平台证书是微信支付平台自己的证书,我们是管不了的,而且是有效期的。微信服务器会定期更换,所以也要求我方定期获取公钥。而且我们只能通过调用接口/v3/certificates来获得,此接口也需要进行签名(可参考上一篇文章)。你可以获取证书后静态放到服务器上,手动更新静态证书;也可以动态获取一劳永逸。本文采取一劳永逸的办法。平台证书接口文档:https://wechatpay-api.gitbook.io/wechatpay-api-v3/jie-kou-wen-dang/ping-tai-zheng-shu3. 证书和回调报文解密为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。也就是说我们拿到响应的信息是被加密的,需要解密后才能获得真正的微信平台证书公钥。响应体大致是这样的,具体根据你调用平台证书接口,应该大差不差是下面这个结构:{ "data": [ { "effective_time": "2020-10-21T14:48:49+08:00", "encrypt_certificate": { // 加密算法 "algorithm": "AEAD_AES_256_GCM", // 附加数据包(可能为空) "associated_data": "certificate", // Base64编码后的密文 "ciphertext": "", // 加密使用的随机串初始化向量) "nonce": "88b4e15a0db9" }, "expire_time": "2025-10-20T14:48:49+08:00", // 证书序列号 "serial_no": "217016F42805DD4D5442059D373F98BFC5252599" } ] }你可以使用各种 JSON 类库取得下面方法的参数进行解密以获取证书,同时这里需要用到APIv3密钥,通用的解密方式为:/** * 解密响应体. * * @param apiV3Key API V3 KEY API v3密钥 商户平台设置的32位字符串 * @param associatedData response.body.data[i].encrypt_certificate.associated_data * @param nonce response.body.data[i].encrypt_certificate.nonce * @param ciphertext response.body.data[i].encrypt_certificate.ciphertext * @return the string * @throws GeneralSecurityException the general security exception */ public String decryptResponseBody(String apiV3Key,String associatedData, String nonce, String ciphertext) { try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES"); GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8)); cipher.init(Cipher.DECRYPT_MODE, key, spec); cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8)); byte[] bytes; try { bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext)); } catch (GeneralSecurityException e) { throw new IllegalArgumentException(e); } return new String(bytes, StandardCharsets.UTF_8); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new IllegalStateException(e); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalArgumentException(e); } }回调的请求体也是此方法进行解密。3. 动态刷新然后就能拿到微信平台证书公钥。然后你可以定义个 Map,以证书的序列号为 KEY,以证书为 Value 来动态刷新,关键伪代码:// 定义全局容器 保存微信平台证书公钥 注意线程安全 private static final Map<String, Certificate> CERTIFICATE_MAP = new ConcurrentHashMap<>(); // 下面是刷新方法 refreshCertificate 的核心代码 String publicKey = decryptResponseBody(associatedData, nonce, ciphertext); final CertificateFactory cf = CertificateFactory.getInstance("X509"); ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8)); Certificate certificate = null; try { certificate = cf.generateCertificate(inputStream); } catch (CertificateException e) { e.printStackTrace(); } String responseSerialNo = objectNode.get("serial_no").asText(); // 清理HashMap CERTIFICATE_MAP.clear(); // 放入证书 CERTIFICATE_MAP.put(responseSerialNo, certificate);动态刷新的策略就很好写了:// 当证书容器为空 或者 响应提供的证书序列号不在容器中时 就应该刷新了 if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) { refreshCertificate(); } // 然后调用 Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);4.总结虽然验签你不做可以拿到其它接口的响应结果,但是从资金安全的角度来说这是十分必要的。同时因为微信平台证书不收我方控制,采取动态刷新也会更加方便,不必再担心过期的问题。本文我们通过调用接口拿到密文并解密获得证书。
2021年03月10日
120 阅读
0 评论
0 点赞
2021-03-10
Java中的微信支付(1):API V3版本签名详解
码农小胖哥服务广大软件开发者。个人博客:https://felord.cn1. 前言最近在折腾微信支付,证书还是比较烦人的,所以有必要分享一些经验,减少你在开发微信支付时的踩坑。目前微信支付的 API 已经发展到V3版本,采用了流行的 Restful 风格。今天来分享微信支付的难点——签名,虽然有很多好用的 SDK 但是如果你想深入了解微信支付还是有帮助的。2. API 证书为了保证资金敏感数据的安全性,确保我们业务中的资金往来交易万无一失。目前微信支付第三方签发的权威的 CA 证书(API 证书)中提供的私钥来进行签名。通过商户平台你可以设置并获取 API 证书。{message type="warning"}切记在第一次设置的时候会提示下载,后面就不再提供下载了,具体参考说明。{/message}设置后找到zip压缩包解压,里面有很多文件,对于 JAVA 开发来说只需要关注apiclient_cert.p12这个证书文件就行了,它包含了公私钥,我们需要把它放在服务端并利用 Java 解析.p12文件获取公钥私钥。{message type="warning"}务必保证证书在服务器端的安全,它涉及到资金安全。{/message}2.解析 API 证书接下来就是证书的解析了,证书的解析有网上很多方法,这里我使用比较“正规”的方法来解析,利用 JDK 安全包的java.security.KeyStore来解析。微信支付 API 证书使用了PKCS12算法,我们通过KeyStore来获取公私钥对的载体KeyPair以及证书序列号serialNumber,我封装了工具类(序列号你自己处理):import org.springframework.core.io.ClassPathResource; import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; /** * KeyPairFactory * * @author dax * @since 13:41 **/ public class KeyPairFactory { private KeyStore store; private final Object lock = new Object(); /** * 获取公私钥. * * @param keyPath the key path * @param keyAlias the key alias * @param keyPass password * @return the key pair */ public KeyPair createPKCS12(String keyPath, String keyAlias, String keyPass) { ClassPathResource resource = new ClassPathResource(keyPath); char[] pem = keyPass.toCharArray(); try { synchronized (lock) { if (store == null) { synchronized (lock) { store = KeyStore.getInstance("PKCS12"); store.load(resource.getInputStream(), pem); } } } X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias); certificate.checkValidity(); // 证书的序列号 也有用 String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase(); // 证书的 公钥 PublicKey publicKey = certificate.getPublicKey(); // 证书的私钥 PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem); return new KeyPair(publicKey, storeKey); } catch (Exception e) { throw new IllegalStateException("Cannot load keys from store: " + resource, e); } } }眼熟的可以看出是胖哥 Spring Security 教程中 JWT 用的公私钥提取方法的修改版本,你可以对比下不同之处。这个方法中有三个参数,这里必须要说明一下:keyPath API 证书apiclient_cert.p12的classpath路径,一般我们会放在resources路径下,当然你可以修改获取证书输入流的方式。keyAlias 证书的别名,这个微信的文档是没有的,胖哥通过加载证书时进行 DEBUG 获取到该值固定为Tenpay Certificate 。keyPass 证书密码,这个默认就是商户号,在其它配置中也需要使用就是mchid,就是你用超级管理员登录微信商户平台在个人资料中的一串数字。3.V3 签名微信支付 V3 版本的签名是我们在调用具体的微信支付的 API 时在 HTTP 请求头中携带特定的编码串供微信支付服务器进行验证请求来源,确保请求是真实可信的。签名格式签名串的具体格式,一共五行一行也不能少,每一行以换行符\n结束。HTTP请求方法\n URL\n 请求时间戳\n 请求随机串\n 请求报文主体\nHTTP 请求方法 你调用的微信支付 API 所要求的请求方法,比如 APP 支付为POST。URL 比如 APP 支付文档中为https://api.mch.weixin.qq.com/v3/pay/transactions/app,除去域名部分得到参与签名的 URL。如果请求中有查询参数,URL 末尾应附加有'?'和对应的查询字符串。这里为/v3/pay/transactions/app。请求时间戳 服务器系统时间戳,保证服务器时间正确并利用System.currentTimeMillis() / 1000获取即可。请求随机串 找个工具类生成类似593BEC0C930BF1AFEB40B4A08C8FB242的字符串就行了。请求报文主体 如果是GET请求直接为空字符"" ;当请求方法为POST或PUT时,请使用真实发送的JSON报文。图片上传 API,请使用meta对应的JSON报文。生成签名然后我们使用商户私钥对按照上面格式的待签名串进行 SHA256 with RSA 签名,并对签名结果进行Base64 编码得到签名值。对应的核心 Java 代码为:/** * V3 SHA256withRSA 签名. * * @param method 请求方法 GET POST PUT DELETE 等 * @param canonicalUrl 例如 https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1 * @param timestamp 当前时间戳 因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致 * @param nonceStr 随机字符串 要和TOKEN中的保持一致 * @param body 请求体 GET 为 "" POST 为JSON * @param keyPair 商户API 证书解析的密钥对 实际使用的是其中的私钥 * @return the string */ @SneakyThrows String sign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair) { String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body) .collect(Collectors.joining("\n", "", "\n")); Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(keyPair.getPrivate()); sign.update(signatureStr.getBytes(StandardCharsets.UTF_8)); return Base64Utils.encodeToString(sign.sign()); }4. 使用签名签名生成后会同一些参数组成一个Token放置到对应 HTTP 请求的Authorization请求头中,格式为: Authorization: WECHATPAY2-SHA256-RSA2048 {Token} Token由以下五部分组成:发起请求的商户(包括直连商户、服务商或渠道商)的商户号mchid商户 API 证书序列号serial_no,用于声明所使用的证书请求随机串nonce_str时间戳timestamp签名值signatureToken生成的核心代码:/** * 生成Token. * * @param mchId 商户号 * @param nonceStr 随机字符串 * @param timestamp 时间戳 * @param serialNo 证书序列号 * @param signature 签名 * @return the string */ String token(String mchId, String nonceStr, long timestamp, String serialNo, String signature) { final String TOKEN_PATTERN = "mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""; // 生成token return String.format(TOKEN_PATTERN, wechatPayProperties.getMchId(), nonceStr, timestamp, serialNo, signature); }将生成的Token按照上述格式放入请求头中即可完成签名的使用。5.总结本文我们对微信支付 V3 版本的难点签名以及签名的使用进行了完整的分析,同时对 API 证书的解析也进行了讲解,相信能够帮助你在支付开发中解决一些具体的问题。
2021年03月10日
152 阅读
0 评论
0 点赞
2021-03-10
用Java实现一个抽奖系统(附完整代码)
来源:https://blog.csdn.net/qq_44140450需求分析1)实现三个基本功能:登录、注册、抽奖。2)登录:用户输入账号密码进行登录,输入账号后会匹配已注册的用户,若输入用户不存在则退出,密码有三次输入机会,登录成功后主界面会显示已登录用户的账号信息。3)注册:用户首先输入账号名称,系统查询此名称是否存在,如存在则请求用户换一个名称,否则进入密码输入,密码要求6位数字字符串,注册成功后,系统随机分配一个与已有用户不重复的四位数字id编号。4)抽奖:功能实现前提:需有用户处于登录状态。该前提满足时,系统从已存在用户中随机抽取5位不同的用户标记为幸运用户,并判断正在登录状态的用户是否被抽中。5)数据存储:采用文件系统,导入java.io.*包,6)数据结构:登录用户信息保存于ArrayList,幸运用户编号和id保存于长度为5的HasMap 其中id为Key,name为Value。实现结果1)登录:2)注册:3)抽奖:注意事项运行代码之前务必在user.txt中创建五个以上的用户。完整代码:import java.util.Scanner; import java.util.ArrayList; import java.io.*; import java.util.StringTokenizer; public class Dos { static boolean logined=false; public static void main(String[] args) { User user=new User(); int k=0; while( (k=Main(user))>=1&&k<5){ switch (k){ case 1: System.out.print((k=user.login(user))==-1?"此用户不存在!\n":""); System.out.print((k==-2)?"===<<警告>>用户:["+user.userName+"]已处于登录状态,无需重复登录!\n":""); break; case 2: user.regist(); break; case 3: user.getLuckly(); break; default:System.exit(0); } } } static int Main(User user){ System.out.println("**********************************************"); System.out.println("********************主菜单********************"); System.out.println("**********************************************"); System.out.println("****** <1> 登 录 ******"); System.out.println("****** <2> 注 册 ******"); System.out.println("****** <3> 抽 奖 ******"); System.out.println("****** <4> 退 出 ******"); System.out.println("**********************************************"); System.out.println("=============================================="); System.out.println(logined ? "-[已登录]- (1)用户名:"+user.userName+" (2)用户账号:"+user.userId:"-[未登录]- (1)用户名:NaN (2)用户账号:NaN"); System.out.println("=============================================="); System.out.print("###===>请输入您的选择:"); return (new Scanner(System.in)).nextInt(); } } public class User{ String userName,userId,userPwd; public User(){} public User(String userName, String userId, String userPwd) { this.userName = userName; this.userId = userId; this.userPwd = userPwd; } public String getUserName() { return userName; } public void setUserName(String userName) { if(!userName.equals("")) { ArrayList<String> temp; this.userId=(temp=(new operatorFile(this.userName = userName)).getUserMess(0)).get(1); this.userPwd=temp.get(2); } } public String getUserId() { return userId; } public String setUserId() { String userId=""; while((new operatorFile(userId=String.valueOf ((int) (Math.random()*9000+1000)))).getUserMess(1).size()>0){ } return (this.userId = userId); } public String getUserPwd() { return userPwd; } public int login(User u) { int inputTimes=3; Scanner scanner=new Scanner(System.in); operatorFile getUserMessage=new operatorFile(); System.out.print("======>请输入您的用户名:"); String uName=""; getUserMessage.setUser(uName=scanner.nextLine()); ArrayList<String> userMess=getUserMessage.getUserMess(0); if(userMess.size()<1) return -1;//返回-1表示用户不存在 if (uName.equals(userName)) return -2;//返回-2表示用户重复登录 System.out.print("======>请输入您的登录密码:"); while(!scanner.next().equals(userMess.get(2))&&inputTimes>0) System.out.print("===>密码输入错误!"+((--inputTimes)>0?"您还剩"+inputTimes+"次机会!":"三次机会已经用完了!输入任意退出")); System.out.println(inputTimes>0?"==>登录成功!您本次输入密码"+(4-inputTimes)+"次!":"==>登录失败!"); setUserName(inputTimes>0?uName:""); Dos.logined=inputTimes>0?true:false; return 0; } public void regist() { User u=new User(); Scanner scanner=new Scanner(System.in); System.out.print("===>请输入新的用户名:"); String name; while(new operatorFile(name=scanner.nextLine()).getUserMess(0).size() > 0) System.out.print("已存在此用户,注册失败!\n===>请重新输入新的用户名:"); System.out.print("======>请设置您的(六位数字)登录密码:"); String regex = "[0-9]{6}", pwd; while (!(pwd = scanner.nextLine()).matches(regex)) System.out.print("==>密码格式不正确,请重新设置您的(六位数字)登录密码:"); System.out.println("已为用户:"+(u.userName=name)+" 生成唯一ID: "+(u.userPwd=pwd)); (new operatorFile()).writeUserMess(u); System.out.println("=======>注册成功!"); } public static HashMap<String,String> lucklyUsers=new HashMap<>(); public void getLuckly() { if (!Dos.logined) { System.out.println("===>警告:没有用户登录,无法抽奖!"); return ; } while(lucklyUsers.size()<5) { String id=""; ArrayList<String> u; while((u=(new operatorFile(id=String.valueOf ((int) (Math.random()*9000+1000)))).getUserMess(1)).size()<1){ } lucklyUsers.put(u.get(1),u.get(0)); } Iterator iterator=lucklyUsers.entrySet().iterator(); int no=1; boolean LUCKLY=false; System.out.println("====>恭喜以下用户获得幸运称号:"); while(iterator.hasNext()){ Map.Entry entry=(Map.Entry) iterator.next(); System.out.println("幸运用户["+(no++)+"] 用户名:"+entry.getValue()+" 用户编号:"+entry.getKey()); LUCKLY = entry.getKey().equals(this.userId) ? true : LUCKLY; } System.out.println(LUCKLY?"=========>恭喜您在本次抽奖中获得幸运称号!":"=========>很遗憾,今日您未获奖 !-_-!"); } public String toString(){ return this.userName+" "+this.userId+" "+this.userPwd; } } public class operatorFile { String user; public void setUser(String user) { this.user = user; } public operatorFile(String user) { this.user = user; } public operatorFile() { } public ArrayList<String> getUserMess(int index){ ArrayList<String> temp=new ArrayList<String>(); File file=new File("user.txt"); String line=""; try{ BufferedReader br=new BufferedReader(new FileReader(file)); while ((line = br.readLine())!=null && line!="\n"){ temp.clear(); StringTokenizer sk=new StringTokenizer(line); while (sk.hasMoreTokens()) { temp.add(sk.nextToken()); } if (temp.get(index).equals(this.user)) break; } } catch(IOException e){} return (line==null)?new ArrayList<String>():temp; } public void writeUserMess(User u){ try{ BufferedWriter bw=new BufferedWriter(new FileWriter(new File("user.txt"),true)); bw.write(u.toString()+"\n"); bw.close(); } catch (IOException e){ } } }
2021年03月10日
140 阅读
0 评论
0 点赞
2021-03-10
红黑树硬核讲解
1 引言预防针:红黑树本来就是基本算法中的难点,所以看此文时建议先有点预备心理或知识铺垫,没接触过RBT而直接看此文的话,绝对懵逼。为了数据的查询跟增删方便,系统引入了二叉查找树,它具有左节点 < 跟节点 <右节点 的属性,但是这种设定跟数据的插入顺序有很大关系,比如你插入的是1234,二叉查找树会退化为链表。为了避免链表结构的出现,研究者们又提出了平衡二叉树跟红黑树。平衡二叉树要求任意一个节点的左深度跟右深度差值绝对值不能大于1,如果插入后超过了1会通过左右各种旋转来更改连接的变化,最终实现左右深度差不大于1的这个要求。平衡二叉树的深度要求太过完美,当涉及大量增删时,可能会太多时间用在调节平衡上,为了平衡投入跟产出比,又设计了红黑树。红黑树算是一个比较复杂的数据结构了,除非你面字节,可能让你手写红黑树。一般情况下你只要说出红黑树构造的五大背后逻辑,展现你对底层数据结构的深度跟广度即可。本文不会着重说红黑树的增删过程,因为你百度看下权威教程或源码,然后跟着追踪就知道大致流程了,本文会说下红黑树为何如此设计,它跟2-3树有啥联系。2 2-3 树2.1 定义为了保证查找树的平衡性,需要一些灵活性,因此我们允许树中的一个结点保存多个键。2结点:含有一个键和两条链接,左链接指向的2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。3结点:含有两个键和三条链接,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。4节点:含有三个键和四条链接,大致的思路跟3节点类似。需注意在2-3树中,4节点是短暂存在的,会被转化为2节点或3节点。2.2 查找要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中的任何一个相等,查找命中。否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这是个空链接,查找未命中,可以发现跟简单的二叉树查找类似。2.3 插入要在2-3树中插入一个新结点,我们可以和二叉查找树一样先进行一次未命中的查找,然后把新结点挂在树的底部。但这样的话树无法保持完美平衡性。使用2-3树的主要原因就在于它能够在插入之后继续保持平衡。如果未命中的查找结束于一个2结点:只要把这个2结点替换为一个3结点,将要插入的键保存在其中即可。只有一个3结点的树,向其插入一个新数据:此时我们可以创建个临时4节点,然后将其转化为由3个2节点组成的2-3树向一个父结点为2结点的3结点中插入新键:此时先将组成个临时4节点,然后将中间数提到上面跟父节点融合为一个3节点,这样树的高度没变。4.向一个父结点为3结点的3结点中插入新键4:跟上面套路类似,不断将中位数的数据往上提,直到遇到个2节点,或者到达了根节点然后进行拆分。插入总结:1.先找插入结点,若结点是2结点,则直接插入。如结点3结点,则插入使其临时容纳这个元素,然后分裂此结点,把中间元素移到其父结点中。对父结点亦如此处理。(中键一直往上移,直到找到空位,在此过程中没有空位就先搞个临时的,再分裂。)2.2-3树插入算法的根本在于这些变换都是局部的:除了相关的结点和链接之外不必修改或者检查树的其他部分。每次变换中,变更的链接数量不会超过一个很小的常数。所有局部变换都不会影响整棵树的有序性和平衡性。2.4 删除2-3树的删除分为两种情况。1.如果待删除元素在3节点,那么可以直接将这个元素删除,删除这个元素不会引起高度的变化。当待删除元素在2节点时,由于删除这个元素会导致2节点失去唯一的元素,引发树中某条路径的高度发生变化,为维持平衡,此时有两种方法。1.先删除再对2-3树进行平衡调整。2.想办法让这个被删除的元素不可能出现在2节点中。如果发现删除元素树2节点则会从兄弟节点或父节点借个元素,当前2节点变为3节点或临时4节点,然后再删除目标数据。2.5 构造和标准的二叉查找树由上向下生长不同,2-3树的生长是由下向上的。2.6 优点、缺点优点:2-3树在最坏情况下仍有较好的性能。每个操作中处理每个结点的时间都不会超过一个很小的常数,且这两个操作都只会访问一条路径上的结点,所以任何查找或者插入的成本都肯定不会超过对数级别。完美平衡的2-3树要平展的多。例如含有10亿个结点的一颗2-3树的高度仅在19到30之间。我们最多只需要访问30个结点就能在10亿个键中进行任意查找和插入操作。缺点:我们需要维护两种不同类型的结点,查找和插入操作的实现需要大量的代码,而且它们所产生的额外开销可能会使算法比标准的二叉查找树更慢。平衡一棵树的初衷是为了消除最坏情况,但我们希望这种保障所需的代码能够越少越好,越简单越好,显然2-3树也不太适合。既然已经懂了2-3树的实现,接下来我们对2-3树稍微变型下就是红黑树了,你可以认为红黑树的本质其实是对概念模型2-3-4树的一种实现。3 红黑树3.1 2-3树跟红黑树关联由于直接进行不同节点间的转化会造成较大的开销,所以选择以二叉树为基础,在二叉树的属性中加入一个颜色属性来表示2-3树中不同的节点。1.2-3树中的2节点对应着红黑树中的黑色节点。2.2-3树中的非2节点是以红节点 + 黑节点的方式存在,红节点的意义是与黑色父节点结合,表达着2-3树中的3,4节点。有的书上把红色说成了红色链接,也是一直理解方法。先看2-3树到红黑树的节点转换。2节点直接转化为黑色节点。3节点这里可以有两种表现形式,左倾红节点或者右倾红节点。而4节点被强制要求转化为一个黑父带着左右两个红色儿子。由于3节点转化到时候可以左倾也可以右倾,如果查看算法书籍,你会发现为了简单化,算法书籍中统一规定只用左倾红黑树。红黑树跟2-3树转化到时候,可以认为将红色节点顺时针上升45度,来跟它到父节点保持平衡,再将红色到跟父节点看作一个整体。可以发现黑色节点才会真正在2-3树中增加高度,所以红黑树的完美平衡其实等价2-3树的根节点到叶子节点到距离相等。所以说红黑树是2-3树或2-3-4树概念模型的一种实现。1.在算法导论中红黑树树基于2-3-4树实现的。2.在算法4中红黑树树基于2-3树实现的,并且要求3节点在红黑树中必须以左倾红色节点来表示。3.2-3树肯定比2-3-4树简单,所以接下来主要基于2-3树说。3.2 红黑树基础定义跟旋转3.2.1 五大法则1.节点有黑色跟红色两种:至于为何有红色节点,在2-3树中已经说过了。2.根节点必须是黑色:2-3树中如果根节点树2节点那本来就是黑色,如果是3节点就用大的当黑根节点,小的当左倾红节点。3.叶子节点为不存数据且都是黑色:主要是为了在插入跟删除时候方便操作。4.任意节点到叶子节点经过的黑色节点数目相同:红黑树中的红节点是和黑色父节点绑定的,在2-3树中本来就是同一层的,只有黑色节点才会在2-3树中真正贡献高度,由于2-3树的任一节点到空链接距离相同,因此反应在红黑树中就是黑色完美平衡。5.不会有连续的红色节点:2-3树中本来就规定没有4节点,2-3-4树中虽然有4节点,但是要求在红黑树中体现为一黑色节点带两红色儿子,分布左右,所以也不会有连续红节点。3.2.2 左旋跟右旋红黑树要求新插入数据颜色是红色,黑色是改变后的结果。红黑树的核心是左旋跟右旋。3.3 左倾红黑树插入左倾红黑树的插入一共有三种可能的情况。1.待插入元素比黑父大,插在了黑父的右边,而黑父左边是红色儿子。这种情况会导致在红黑树中出现右倾红节点。或者黑父左边为空也会出现右倾。2.待插入元素比红父小,且红父自身就是左倾,待插入数据比红父左节点还小,形成了连续的红节点。1.对红父的父亲节点进行一次右旋转。2.将数据变化为情况1的状态处理。3.待插入元素比红父大,且红父自身就是左倾。待插入数据比红父左节点大,形成了右倾。通过左旋变成情况2处理。整体来说左倾红黑树的插入就是这3种情况来回切换,最终达到平衡。3.4 左倾红黑树删除删除思路是不删除目标数据,而是找到目标数据的前驱节点或后继节点,然后把数据拷贝一份到目标数据进行覆盖。然后转而去删除前驱或后继。删除后再去修补平衡。从宏观上来看从根节点开始查找,全程利用2-3树思维逐层对红黑树调整,每次保证当前节点树2-3树中非2节点,如果是非2节点则看下一层,如果是2节点则根据兄弟节点调整。1.兄弟节点是2节点,从父节点借个数据跟当前节点及兄弟节点形成临时4节点。2.兄弟节点是非2节点,兄弟节点上升一个数据,父节点下降一个数据。删除后就涉及到数据平衡修复了,还是根据2-3树来修复平衡,路上可能会碰到红色右倾节点,遇到就进行一次左旋即可。3.5 工业级红黑树增加这里其实主要参考极客时间小争哥的文章,说下实际工程中红黑树的增删操作,增加主要有3种情况:情况1:关注节点是 a,它的叔叔节点 d 是红色:1.将关注节点 a 的父节点 b、叔叔节点 d 的颜色都设置成黑色。2.将关注节点 a 的祖父节点 c 的颜色设置成红色。3.关注节点变成 a 的祖父节点 c,实现关注节点的迁移。4.跳到情况2或情况3。情况2:关注节点是 a,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的右子节点:1.关注节点变成节点 a 的父节点 b。2.围绕新的关注节点 b 左旋。3.跳到情况3。情况3:如果关注节点是 a,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的左子节点,我们就依次执行下面的操作:1.围绕关注节点 a 的祖父节点 c 右旋。2.将关注节点 a 的父节点 b、兄弟节点 c 的颜色互换,调整结束。3.6 工业级红黑树删除相比插入,删除就难多了!核心思想是找准关注点,根据关注点跟周围节点排布特征按照一定规则调整。主要俩步骤:1.针对删除节点调整后仍要满足节点到叶子节点路径包含相同黑色节点。2.针对关注节点二次调整,防止出现2个红色节点。算法导论中说如果删除黑节点X带来黑色平衡破坏,让X的子节点变为黑-黑或红-黑。意思是既然删除了某个黑色节点,那么必然会破坏以这个黑色节点为路径上的黑色平衡,表现为路径中缺少一个黑,所以要想办法补充一个黑色节点(下面会用黑色圆圈表示)。同时如果一个节点既可以红又可以黑,就用红黑两个组成部分表示。3.6.1 删除第一步情况1:要删除的节点是 a,它只有一个子节点 b:1.删除节点 a,并且把节点 b 替换到节点 a 的位置,这一部分操作跟普通的二叉查找树的删除操作一样。2.节点 a 只能是黑色,节点 b 也只能是红色,其他情况均不符合红黑树的定义。此时把节点 b 改为黑色。调整结束,不需要进行二次调整。情况2:要删除的节点 a 有两个非空子节点,并且它的后继节点就是节点 a 的右子节点 c:1.如果节点 a 的后继节点就是右子节点 c,那 c 肯定没有左子树。将c的颜色变为a的颜色,并且用c来覆盖a。2.如果节点 c 是黑色,为了不违反红黑树的路径相同原则,给节点 c 的右子节点 d 多加一个黑色圆圈,这个时候节点 d 就成了红 - 黑或者黑 - 黑。3.此时关注节点变成了节点 d,第二步的调整操作就会针对关注节点来做。情况3:要删除的是节点 a,它有两个非空子节点,并且节点 a 的后继节点不是a的右子节点:1.找到后继节点 d,并将它删除,删除后继节点 d 的过程参照 CASE 1。2.用d来替换a,并且d的颜色设置的跟a颜色一样。3.如果节点 d 是黑色,为了不违反红黑树路径相同原则,给节点 d 的右子节点 c 多加一个黑色,这个时候节点 c 就成了红 - 黑或者黑 - 黑。4.此时关注节点变成了节点 c,第二步的调整操作就会针对关注节点来做。3.6.2 删除第二步经过初步调整之后,关注节点变成了红 - 黑或者黑 - 黑 节点。针对这个关注节点,再分四种情况来进行二次调整。二次调整是为了让红黑树中不存在相邻的红色节点。情况1:关注节点是 a,它的兄弟节点 c 是红色的,我们就依次进行下面的操作:1.围绕关注节点 a 的父节点 b 左旋。2.关注节点 a 的父节点 b 和祖父节点 c 交换颜色。3.关注节点不变,继续从四种情况中选择适合的规则来调整。情况2:关注节点是 a,它的兄弟节点 c 是黑色,并且节点 c 的左右子节点 d、e 都是黑色:1.将关注节点 a 的兄弟节点 c 的颜色变成红色,因为接下来黑圆圈会上移,那么c比a多个深色。2.从关注节点 a 中去掉一个黑色,此时节点 a 就是单纯的红色或者黑色。3.给关注节点 a 的父节点 b 添加一个黑色,这个时候节点 b 就变成红 - 黑或者黑 - 黑4.关注节点从 a 变成其父节点 b,继续从四种情况中选择符合的规则来调整。情况3:关注节点是 a,它的兄弟节点 c 是黑色,c 的左子节点 d 是红色,c 的右子节点 e 是黑色:1.围绕关注节点 a 的兄弟节点 c 右旋。2.节点 c 和节点 d 交换颜色。3.关注节点不变,跳转到情况4,继续调整。情况4:关注节点 a 的兄弟节点 c 是黑色的,并且 c 的右子节点是红色的,我们就依次进行下面的操作:1.围绕关注节点 a 的父节点 b 左旋。2.将b的颜色复制给c,因为c替代了b的位置。3.将关注节点 a 的父节点 b 的颜色设置为黑色。4.从关注节点 a 中去掉一个黑色,节点 a 就变成了单纯的红色或黑色。5.将关注节点 a 的叔叔节点 e 设置为黑色,调整结束。6.此时a跟d深度是一样的,因为无法判别ad是否为红,直接将b设置为黑的了,此时e提高了一度为保持平衡也设置为黑色的了。3.6.3 删除理解1.多画图,不画图单看代码一会儿就眩晕了。2.插入跟删除算法都是用到了递推,比如插入情况1,情况1的处理之后,关注节点从本身变成了它的祖父红色节点,这就是往根节点递推。不过情况1处理过一次之后,不一定会进入情况2或者情况3,有可能还在情况1。在情况1的情况下一直往根节点走,因为当前节点永远是红色,所以在最后要把根节点涂黑。同时只要进入到情况2、情况3情况,操作就跟上面说过的类似了。3.要记住,除了关注的节点所在的子树,其他的子树本身都是一颗红黑树,它们是满足红黑树的所有特征的。当关注节点往根节点递推时,这个时候关注节点的子树也已经满足了红黑树的定义,我们就不用再去特别关注子树的特征。只要注意关注节点往上的部分。这样就能把问题简化,思考的时候思路会清晰一些。4.再说到删除算法,注意红-黑跟黑-黑存在的原因,为何最终都会走到从兄弟节点的地方做文章来实现最终的平衡。5.删除情况1的目的只是为了能够进入接下来的三个情况中。6.删除情况2的套路又是一个递推思路,关注节点往根节点递推,让其左右子树都满足红黑树的定义。因为往上推,右子树多了一个黑色节点,就把关注节点的兄弟节点变红。7.删除情况3是为了进入删除情况4,提前变色的原因和情况2是一样的,都是为了满足黑色深度相同。同样是归纳推理的思路。都要记住一点,各种情况下的其他子树节点都满足红黑树的定义,需要分类讨论的,都在这几种情况中了。8.可能你看今天看了红黑树的删除你顿悟了,过了半个月又迷糊了。不要怕!因为怕也没用,再看呗。学习红黑树本身也不是为了面试字节去默写,而是去学习思想,锻炼思维,复杂问题简单化,当然了顺带也可以装的一手好B。4 总结本文的重点不在于讲解工业化红黑树的删除跟插入全部过程,只是希望通过2-3树跟左倾红黑树的增删,让大家从本质上理解下红黑树的操作来源。其中工业化删除部分已征得小争哥同意,如果理解了上面的内容,那么你再去看工业化红黑树的操作就手到擒来了。参考红黑树在线演示:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html2-3树到红黑树讲解:https://blog.csdn.net/qq_35190492/article/details/109503539小争哥极客时间红黑树:https://time.geekbang.org/column/article/68976
2021年03月10日
223 阅读
1 评论
0 点赞
2021-03-09
小妹
{music id="316108" autoplay="autoplay"/}{mtitle title="1"/} 今天写写我妹,许诺。 她不曾出现在我的任何一篇文章里,但与我相熟的朋友都知道这个孽障。她对于我的意义,便是使我排除了YY小说里任何关于乱伦诱惑的干扰,无忧无虑地度过了健康的青春期。 说实话,如果你也有个小你两岁,打光着屁股就开始拖着鼻涕抢玩具争宠夺爱,打翻醋坛子互相挤兑,撕烂了脸从床上打到地上再滚下楼梯磕破了脑袋,被她掐哭,被她告刁状,被她举报揭发我早恋,被她搞各种大新闻,然后终于熬到她青春期,出落得亭亭玉立肤如凝脂的时候,你也会像我一样,满眼都是她熊孩子时的影子。 父亲是公务员,小妹是以父亲一己之力,不,是合我妈二人之力偷着生的。户口找人落的,从小学到初中高中,一直到她上了大学,终于尘埃落定。 爸妈给她取了一个美丽温柔的名字,可她如今还没学会温柔。 在青春期猝不及防的某一瞬间,我突然发现她——自己的妹妹,还挺好看的。 我当时便对她说,咱爹娘为了生你,已经用完了老子一生的运气。 她撇嘴无视我的自黑:“人丑多作怪。你丑你的独木桥,我美我的阳关道,关我什么事?” 我说:“你妈的!”她运了一口气,我感觉不妙。 “妈——哥又说你坏话——” 脆生生的,亮晶晶的,我的小妹。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="2"/} 她和我上同一所小学,同一所初中,同一所高中,直到大学才分开。 从小到大,我们都不像。她在学校里轮滑跳舞,唱歌主持,我在台下摊开书写作业。她在光芒四射,我在默默无闻地做一颗石头。等她卸了那跟哪吒一样的妆,放下破音的话筒,我俩就一块儿回家。当然,大多数时间,我们还是默契地保持一段距离,她和她的小姐妹们走在前面,我和我的小伙伴们走在后面。甚至在十五岁之前,我一直没意识到妹妹的含义,也没有丝毫当哥哥的责任感和使命感。 只有出了成绩单的时候,爸爸就会敲着她的脑壳说:多跟你哥学学。你唱歌跳舞,爸妈不限制你,但是你要知道,你的主业是什么。第一,你要从思想上…… 我一直很讨厌我爸在开会时的三三不断式,但是每当这时便非常享受。她低着头,趁爸喝水的时候,恶狠狠地瞪我一眼,我扮个鬼脸回敬她,心里在说:你不是牛逼吗?怎么也有今天啊? 回老家探亲时,在重男轻女思想严重的农村,她也能凭借甜美的嘴巴闯出一片天地。左一口“爷爷”,右一口“奶奶”,声音甜得让人耳根软。刹那间,她久治不愈的公主病瞬间痊愈,腿脚麻利得像是满血复活,择菜洗碗端茶倒水,唠嗑拉呱卖萌扮乖。长辈们纷纷赞不绝口:这妮儿真勤快,是个懂事的娃娃。每每此时,我都黑着脸坐在角落里,活像被打入冷宫的嫔妃。我甚至能感觉到,爷爷奶奶更喜欢她这个孙女,而不是我这个孙儿。 最关键的是,在家里我们俩都是不做家务的,回去了之后她那个殷勤哟,真是酸死我了,看得我浑身汗毛竖立,甜腻的音调儿像白骨精一样阴阳怪气。每年两个假期都是我恶意爆棚的时期,我们会对几乎所有事情产生矛盾。抢淋浴,抢空调,抢电视,抢Wi-Fi,甚至抢马桶。 亲妹妹,不过是一个同住的讨厌鬼。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="3"/} 这平静的一切在我高三时改变了。那年她高一。 我们的高中绝对是一座怪兽育成所,拥有各种各样神秘的传统和高尚的宣言。遍地的术士和法师。 那时我才悲痛地顿悟,我这种只知道看文献的麻瓜并不能改变世界。 于是在高三,我联合另外几个悲痛的麻瓜,成立了我们的校园暴力集团。几战之后,拿下小老虎干翻中老虎,大老虎们也不愿意与我们刀兵相见,独虎不敌群狼。而这几年,我已经从看文献的呆瓜变成恶狗。 那年,许诺高一。 在一个月黑风高的夜晚,我正和兄弟们在学校对面的烧烤摊上喝酒,突然接到她的电话,电话那头传来乒乓的响声和咒骂声,一片嘈杂混乱。我当即买单启程,和小伙伴们杀回学校,七八个小伙伴们站成一个弧,我浑身酒气地搂着她,到各个班里一个个地揪人,一巴掌一巴掌地剁。据后来她讲,那是她第一次感觉我像她哥,那也是我第一次搂着她。 唯一不美好的是,第二天在公告栏上,贴出了我的严重警告处分。我俩正路过,我装作无所谓地嬉皮笑脸,从书包里掏出红色马克笔,写了个“阅”。 身边的她抢过我手中的笔,一笔一画地把她自己的名字落在下面——“许诺”。 她回头,笑得嫣然。 之后她就理所当然地跟着我们鬼混。那时爸妈主要还是关心我的高考,我天天一副无所谓劈开腿让世界来吧的样子,让爹妈操碎了心。这时候角色反转,爸爸开始用三三不断式给我进行思想教育,教育我要安分守己,不要总是搞大新闻。许诺一脸沉痛地看着我,像是看一个不成器的兄长。在教育完毕之后,总会在爸爸转身的一瞬间,看到她的鬼脸。 那段时间兄妹关系融洽到不像话,在学校里经常有人叫她嫂子。她会很认真地对每个人说,你可以侮辱我的审美,但不能高估人类忍耐的底线。 每次都是我掐着她脖子给拎过来,再惨笑着说,这是我妹。 傻×们纷纷摇头:“不像。”{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="4"/} 我们家喝酒绝对是有基因的。以后的酒,基本都是老许、小许和一帮兄弟。 从小会说漂亮话的她喝酒的时候也是如此。碰杯低,落杯脆,一口干了,面颊绯红。 “磊哥哥最仗义了,我敬你一杯。” “坤哥哥最豪爽了,我敬你一杯。 …… 在敬完一圈之后,她醉醺醺的,头发湿答答的。面颊飞雪,眼睛泛潮。软软地站起来,扶着小腹,手臂半弯。 “凯丞哥哥你长得最帅,你做我男朋友吧。” 我刚喝得乐颠颠的,她这话劈头一瀑水,霎时把我浇醒了。 凯丞和我同时说:“我靠。” 我盯着凯丞说:“你,敢!” 凯丞尴尬地看看她,又看看我,六神无主了。 “这不行……”凯丞说。 许诺就吻上去了。 那晚流星扫路面,把我炸成一团暴躁的火。我扶着她推开川流不息的雾,脚下平行出无数条一模一样的路。天上喷涌出贞洁的月光酒,我喝了一壶又一壶。 乳汁般黏稠的初夏,我将毕业。我的妹妹许诺——这只讨厌鬼——也长大了。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="5"/} 在他们分手之后,我并没有和凯丞有什么过节。只是调解过几次,无果也就罢了。正好,我们都要走了。给予她赫赫威名,也让她免受欺负。 在那次表白之后,我便把她当个姑娘来看了,不由自主地琢磨她的心思,总是没来由地小心。那一次表白让我意识到一种巨大的危险,她长大了,不能永远一脸鼻涕地跟在我的身后。那时总觉得她很烦,但她却安全地粘在我的掌心里。 虽然我依旧幼稚,但一到她身上,便觉得自己得像个哥哥。需要肩负许多责任,需要对她宠溺无涯。小时候那些糗事和互相进行的暴力迫害,反而变得温暖。 有好吃的,就想给她吃。身上有两百块钱,恨不得给她两千。不允许她喝酒,她生理期了我就哄她喂她喝热水。那段时间不想交女朋友,只是觉得,一辈子供一个祖宗就够我忙活了,再来一个我可走不开。 像每个平凡的哥哥一样。 那天在“一杯沧海”,我拿着做兼职的钱,请她喝咖啡。 我看着她——自己的妹妹,如痴如醉。 我说:“许诺。” 她说:“咦,咋了?” 我说:“没事儿,我就叫叫你。爸妈没给我起这么好听的名字。” 她一撇嘴,说:“傻×。” 我看着她洁白如鸽羽的皮肤,雕塑般修长的双腿,像爸爸那样,弯弯的眼睛和挺拔的鼻梁,像妈妈那样,纤瘦的腰和渐长的身体。小臂上铺满细细的绒毛,被夕阳一镀,柔软了一层黄昏的云。 许诺十八岁了。 有时想,我们应该是多亲密啊。我们共享一个子宫,我们喝同一个女人的乳汁,冠同一个男人的姓氏。从你的眉眼神态中,能看到自己的影子。就像是看着另外一个自己,自己的另外一种可能。仿佛你是自己的女儿和母亲。我们家族的源头在那里,你我是两条河岸,或是并肩的浪潮。 我心情低落时,她仿佛能感应得到。总是打电话来,跟我有一搭没一搭地扯淡,没大没小的,叫我名字的时候多,叫我“哥”的时候少。 我想,岁月啊,你就把我的妹妹定格在十八岁吧。不要让她嫁人,不要让她和我一同随着时间的队伍逃亡。让她唱歌和画画,撒娇与任性。让她一直有梦想,喜欢好看的男生。让她不尝辛苦,也不必成熟。 她总是说:“许耀方,还有我呢,没事儿。实在不行咱回家。” 我总是说:“许诺,还有我呢,没事儿没事儿。你哭啥?你哭我还得给你擦。” 这个家有四口人,生命很沉,父母是生命的根,我俩是生命的肩。 一起扛,就很稳。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="6"/} 1992年。 一位年轻母亲的妊娠期,她的丈夫——年轻的许先生,通过医院走后门,看着彩超,断定是个女孩儿。 他与妻子商定,给孩子起名为许诺。是个充满诚恳和希望的名字。 1993年1月,新生的孩子满头黑发,还长着一只粉红的小鸡鸡。那是除夕夜,医院里只出生了一个孩子,没有抱错的可能性。许先生感慨自己学艺不精,只能把原来买的女婴装收起来,再买男孩子的衣服。 1995年,孩子的母亲再次怀孕,已过而立之年的许先生又看了看彩超,都能看清孩子的眉眼。许先生这次没看错,是个女孩儿,没跑。 许先生想,留住这个孩子吧,但他是公务员,1996年,那一切仍旧困难重重。 生下来,就叫许诺。 可她最终,未曾来过。 在被告知此事时,我曾抱有许多幻想,如果这个孩子——我的妹妹,生下来后,她会不会尿我的床,抢我的玩具,扯我的头发,告我的刁状? 会不会真如爸爸描述的那般好看?出落得亭亭玉立? 会不会与我最深爱的兄弟,谈一场恋爱? 我的生命,会不会因为她而不同? 我会不会更沉稳、踏实、成熟并且忍耐? 毕竟,成为兄长是成为父亲之前,第一次可以成为小男子汉的机会。 可是没有,这一切,这篇文章,全存在于我的想象当中。 若她当年来过,如今也有十八岁了。 而我也看不到另外一个自己,也保护不了不存在的她。到底,我还是没有亲生妹妹。这是这个国家,这个年代,给予我的毕生遗憾。 我想,若我有个女儿,就叫她许诺吧。
2021年03月09日
323 阅读
2 评论
1 点赞
2021-03-08
再见命令行!K8S傻瓜式安装,图形化管理真香!
之前我们一直都是使用命令行来管理K8S的,这种做法虽然对程序员来说看起来很炫酷,但有时候用起来还是挺麻烦的。今天我们来介绍一个K8S可视化管理工具Rancher,使用它可以大大减少我们管理K8S的工作量,希望对大家有所帮助!Rancher简介Rancher是为使用容器的公司打造的容器管理平台。Rancher简化了使用K8S的流程,开发者可以随处运行K8S,满足IT需求规范,赋能DevOps团队。Rancher安装Rancher已经内置K8S,无需再额外安装。就像我们安装好Minikube一样,K8S直接就内置了。首先下载Rancher镜像:docker pull rancher/rancher:v2.5-head下载完成后运行Rancher容器,Rancher运行起来有点慢需要等待几分钟:docker run -p 80:80 -p 443:443 --name rancher \ --privileged \ --restart=unless-stopped \ -d rancher/rancher运行完成后就可以访问Rancher的主页了,第一次需要设置管理员账号密码,访问地址:https://主机IP设置下Rancher的Server URL,一个其他Node都可以访问到的地址,如果我们要安装其他Node的话需要用到它;Rancher使用我们首先来简单使用下Rancher。在首页我们可以直接查看所有集群,当前我们只有安装了Rancher的集群;点击集群名称可以查看集群状态信息,也可以点击右上角的按钮来执行kubectl命令;点击仪表盘按钮,我们可以查看集群的Dashboard,这里可以查看的内容就丰富多了,Deployment、Service、Pod信息都可以查看到了。Rancher实战之前我们都是使用命令行的形式操作K8S,这次我们使用图形化界面试试。还是以部署SpringBoot应用为例,不过先得部署个MySQL。部署MySQL首先我们以yaml的形式创建Deployment,操作路径为Deployments->创建->以YAML文件编辑;Deployment的yaml内容如下,注意添加namespace: default这行,否则会无法创建;apiVersion: apps/v1 kind: Deployment metadata: # 指定Deployment的名称 name: mysql-deployment # 指定Deployment的空间 namespace: default # 指定Deployment的标签 labels: app: mysql spec: # 指定创建的Pod副本数量 replicas: 1 # 定义如何查找要管理的Pod selector: # 管理标签app为mysql的Pod matchLabels: app: mysql # 指定创建Pod的模板 template: metadata: # 给Pod打上app:mysql标签 labels: app: mysql # Pod的模板规约 spec: containers: - name: mysql # 指定容器镜像 image: mysql:5.7 # 指定开放的端口 ports: - containerPort: 3306 # 设置环境变量 env: - name: MYSQL_ROOT_PASSWORD value: root # 使用存储卷 volumeMounts: # 将存储卷挂载到容器内部路径 - mountPath: /var/log/mysql name: log-volume - mountPath: /var/lib/mysql name: data-volume - mountPath: /etc/mysql name: conf-volume # 定义存储卷 volumes: - name: log-volume # hostPath类型存储卷在宿主机上的路径 hostPath: path: /home/docker/mydata/mysql/log # 当目录不存在时创建 type: DirectoryOrCreate - name: data-volume hostPath: path: /home/docker/mydata/mysql/data type: DirectoryOrCreate - name: conf-volume hostPath: path: /home/docker/mydata/mysql/conf type: DirectoryOrCreate其实我们也可以通过页面来配置Deployment的属性,如果你对yaml中的配置不太熟悉,可以在页面中修改属性并对照下,比如hostPath.type这个属性,一看就知道有哪些了;之后以yaml的形式创建Service,操作路径为Services->创建->节点端口->以YAML文件编辑;Service的yaml内容如下,namespace属性不能少;apiVersion: v1 kind: Service metadata: # 定义空间 namespace: default # 定义服务名称,其他Pod可以通过服务名称作为域名进行访问 name: mysql-service spec: # 指定服务类型,通过Node上的静态端口暴露服务 type: NodePort # 管理标签app为mysql的Pod selector: app: mysql ports: - name: http protocol: TCP port: 3306 targetPort: 3306 # Node上的静态端口 nodePort: 30306部署完成后需要新建mall数据库,并导入相关表,表地址:https://github.com/macrozheng/mall-learning/blob/master/document/sql/mall.sql这里有个比较简单的方法来导入数据库,通过Navicat创建连接,先配置一个SSH通道;接下来要获得Rancher容器运行的IP地址(在Minikube中我们使用的使用Minikube的地址);docker inspect rancher |grep IPAddress之后我们就可以像在Linux服务器上访问数据库一样访问Rancher中的数据库了,直接添加Rancher的IP和数据库端口即可。部署SpringBoot应用以yaml的形式创建SpringBoot应用的Deployment,操作路径为Deployments->创建->以YAML文件编辑,配置信息如下;apiVersion: apps/v1 kind: Deployment metadata: namespace: default name: mall-tiny-fabric-deployment labels: app: mall-tiny-fabric spec: replicas: 1 selector: matchLabels: app: mall-tiny-fabric template: metadata: labels: app: mall-tiny-fabric spec: containers: - name: mall-tiny-fabric # 指定Docker Hub中的镜像地址 image: macrodocker/mall-tiny-fabric:0.0.1-SNAPSHOT ports: - containerPort: 8080 env: # 指定数据库连接地址 - name: spring.datasource.url value: jdbc:mysql://mysql-service:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai # 指定日志文件路径 - name: logging.path value: /var/logs volumeMounts: - mountPath: /var/logs name: log-volume volumes: - name: log-volume hostPath: path: /home/docker/mydata/app/mall-tiny-fabric/logs type: DirectoryOrCreate以yaml的形式创建Service,操作路径为Services->创建->节点端口->以YAML文件编辑,配置信息如下;apiVersion: v1 kind: Service metadata: namespace: default name: mall-tiny-fabric-service spec: type: NodePort selector: app: mall-tiny-fabric ports: - name: http protocol: TCP port: 8080 targetPort: 8080 # Node上的静态端口 nodePort: 30180创建成功后,在Deployments标签中,我们可以发现实例已经就绪了。外部访问应用依然使用Nginx反向代理的方式来访问SpringBoot应用。由于Rancher服务已经占用了80端口,Nginx服务只能重新换个端口了,这里运行在2080端口上;docker run -p 2080:2080 --name nginx \ -v /mydata/nginx/html:/usr/share/nginx/html \ -v /mydata/nginx/logs:/var/log/nginx \ -v /mydata/nginx/conf:/etc/nginx \ -d nginx:1.10创建完Nginx容器后,添加配置文件mall-tiny-rancher.conf,将mall-tiny.macrozheng.com域名的访问反向代理到K8S中的SpringBoot应用中去;server { listen 2080; server_name mall-tiny.macrozheng.com; #修改域名 location / { proxy_set_header Host $host:$server_port; proxy_pass http://172.17.0.3:30180; #修改为代理服务地址 index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }再修改访问Linux服务器的本机host文件,添加如下记录;192.168.5.46 mall-tiny.macrozheng.com之后即可直接在本机上访问K8S上的SpringBoot应用了,访问地址:http://mall-tiny.macrozheng.com:2080/swagger-ui.html总结使用Rancher可视化管理K8S还真是简单,大大降低了K8S的部署和管理难度。一个Docker命令即可完成部署,可视化界面可以查看应用运行的各种状态。K8S脚本轻松执行,不会写脚本的图形化界面设置下也能搞定。总结一句:真香!都看到这了,确定不来个点赞,在看鼓励下么?这将是我创造更多优质文章的最大动力!参考资料Rancher官方文档项目源码地址
2021年03月08日
155 阅读
0 评论
0 点赞
2021-03-08
一个注解搞定 SpringBoot 接口防刷
说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供参考。技术要点:springboot的基本知识,redis基本操作。1.首先是写一个注解类:import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { int seconds(); int maxCount(); boolean needLogin()default true; }2.接着就是在Interceptor拦截器中实现:import com.alibaba.fastjson.JSON; import com.example.demo.action.AccessLimit; import com.example.demo.redis.RedisService; import com.example.demo.result.CodeMsg; import com.example.demo.result.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.OutputStream; @Component public class FangshuaInterceptor extends HandlerInterceptorAdapter { @Autowired private RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断请求是否属于方法的请求 if(handler instanceof HandlerMethod){ HandlerMethod hm = (HandlerMethod) handler; //获取方法中的注解,看是否有该注解 AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if(accessLimit == null){ return true; } int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); boolean login = accessLimit.needLogin(); String key = request.getRequestURI(); //如果需要登录 if(login){ //获取登录的session进行判断 //..... key+=""+"1"; //这里假设用户是1,项目中是动态获取的userId } //从redis中获取用户访问的次数 AccessKey ak = AccessKey.withExpire(seconds); Integer count = redisService.get(ak,key,Integer.class); if(count == null){ //第一次访问 redisService.set(ak,key,1); }else if(count < maxCount){ //加1 redisService.incr(ak,key); }else{ //超出访问次数 render(response,CodeMsg.ACCESS_LIMIT_REACHED); //这里的CodeMsg是一个返回参数 return false; } } return true; } private void render(HttpServletResponse response, CodeMsg cm)throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); String str = JSON.toJSONString(Result.error(cm)); out.write(str.getBytes("UTF-8")); out.flush(); out.close(); } }3.再把Interceptor注册到springboot中import com.example.demo.ExceptionHander.FangshuaInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Autowired private FangshuaInterceptor interceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor); } }4.接着在Controller中加入注解import com.example.demo.result.Result; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class FangshuaController { @AccessLimit(seconds=5, maxCount=5, needLogin=true) @RequestMapping("/fangshua") @ResponseBody public Result<String> fangshua(){ return Result.success("请求成功"); } }
2021年03月08日
170 阅读
0 评论
0 点赞
2021-03-08
springboot中使用自定义注解实现策略模式,去除工厂模式的switch或ifelse,实现新增策略代码零修改
整体思路就是通过注解在策略类上指定约定好的type,项目启动之后将所有有注解的type获取到,根据type存储,然后在业务中根据type获取对应的策略即可。本文已模拟订单业务进行演示,根据订单的type,需要不同的处理逻辑,比如,免费订单,半价订单等。1.策略接口和实现/** * 处理订单策略 */ public interface OrderStrategy { void handleOrder(Order order); }@Component @HandlerOrderType(Order.FREE) //使用注解标明策略类型 public class FreeOrderStrategy implements OrderStrategy { @Override public void handleOrder(Order order) { System.out.println("----处理免费订单----"); } }@Component @HandlerOrderType(Order.HALF) public class HalfOrderStrategy implements OrderStrategy { @Override public void handleOrder(Order order) { System.out.println("----处理半价订单----"); } }@Component @HandlerOrderType(Order.DISCOUT) public class DiscoutOrderStrategy implements OrderStrategy { @Override public void handleOrder(Order order) { System.out.println("----处理打折订单----"); } }2.自定义策略注解@Target(ElementType.TYPE) //作用在类上 @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited //子类可以继承此注解 public @interface HandlerOrderType { /** * 策略类型 * @return */ int value(); }3.业务实体public class Order { public static final int FREE=1; //免费订单 public static final int HALF=2; //半价订单 public static final int DISCOUT=3; //打折订单 private String name; private Double price; private Integer type;//订单类型 public static Order build(){ return new Order(); }4.核心功能实现/** * 根据订单类型返回对应的处理策略 */ @Component public class HandlerOrderContext { @Autowired private ApplicationContext applicationContext; //存放所有策略类Bean的map public static Map<Integer, Class<OrderStrategy>> orderStrategyBeanMap= new HashMap<>(); public OrderStrategy getOrderStrategy(Integer type){ Class<OrderStrategy> strategyClass = orderStrategyBeanMap.get(type); if(strategyClass==null) throw new IllegalArgumentException("没有对应的订单类型"); //从容器中获取对应的策略Bean return applicationContext.getBean(strategyClass); } }/** * 策略核心功能,获取所有策略注解的类型 * 并将对应的class初始化到HandlerOrderContext中 */ @Component public class HandlerOrderProcessor implements ApplicationContextAware { /** * 获取所有的策略Beanclass 加入HandlerOrderContext属性中 * @param applicationContext * @throws BeansException */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { //获取所有策略注解的Bean Map<String, Object> orderStrategyMap = applicationContext.getBeansWithAnnotation(HandlerOrderType.class); orderStrategyMap.forEach((k,v)->{ Class<OrderStrategy> orderStrategyClass = (Class<OrderStrategy>) v.getClass(); int type = orderStrategyClass.getAnnotation(HandlerOrderType.class).value(); //将class加入map中,type作为key HandlerOrderContext.orderStrategyBeanMap.put(type,orderStrategyClass); }); } }5.业务service使用@Component public class OrderServiceImpl implements OrderService { @Autowired HandlerOrderContext handlerOrderContext; @Override public void handleOrder(Order order) { //使用策略处理订单 OrderStrategy orderStrategy = handlerOrderContext.getOrderStrategy(order.getType()); orderStrategy.handleOrder(order); } }6.controller测试@RestController @RequestMapping("/order") public class OrderController { @Autowired OrderService orderService; @GetMapping("/handler/{type}") public void handleOrder(@PathVariable Integer type){ Order order = Order.build() .add("name", "微信订单") .add("price", 99.9) .add("type", type); orderService.handleOrder(order); } }7.测试再添加策略添加实现类,启用注解即可!省去了工厂模式,直接用注解实现,避免修改工厂类,这里贴一个我们之前项目的工厂类实现:
2021年03月08日
219 阅读
0 评论
0 点赞
2021-03-08
23 种设计模式的设计理念
什么是设计模式设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。简单说:模式:在某些场景下,针对某类问题的某种通用的解决方案场景:项目所在的环境问题:约束条件,项目目标等解决方案:通用、可复用的设计,解决约束达到目标设计模式的三个分类创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。结构型模式:把类或对象结合在一起形成一个更大的结构。行为型模式:类和对象如何交互,及划分责任和算法。各分类中模式的关键点创建型模式单例模式:某个类只能有一个实例,提供一个全局的访问点。简单工厂:一个工厂类根据传入的参量决定创建出那一种产品类的实例。工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。原型模式:通过复制现有的实例来创建新的实例。结构型模式适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。装饰模式:动态的给对象添加新的功能。代理模式:为其他对象提供一个代理以便控制这个对象的访问。亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。行为型模式模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。状态模式:允许一个对象在其对象内部状态改变时改变它的行为。观察者模式:对象间的一对多的依赖关系。备忘录模式:在不破坏封装的前提下,保持对象的内部状态。中介者模式:用一个中介对象来封装一系列的对象交互。命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
2021年03月08日
126 阅读
0 评论
0 点赞
2021-03-08
教你一行代码激活win10
# 1.首先以管理员的身份进入powershell界面 # 2.在powershell界面输入:slmgr /skms kms.03k.org # 3.在powershell界面输入:slmgr /ato
2021年03月08日
293 阅读
0 评论
0 点赞
2021-03-08
SpringBoot--防止重复提交(分布式锁实现)
防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(Guava)即可,如果是分布式部署,则需要使用分布式锁(可以使用zk分布式锁或者redis分布式锁),本文的分布式锁以redis分布式锁为例。1.自定义分布式锁注解import java.lang.annotation.*; import java.util.concurrent.TimeUnit; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheLock { //redis锁前缀 String prefix() default ""; //redis锁过期时间 int expire() default 5; //redis锁过期时间单位 TimeUnit timeUnit() default TimeUnit.SECONDS; //redis key分隔符 String delimiter() default ":"; }2.自定义key规则注解由于redis的key可能是多层级结构,例如 redistest:demo1:token:kkk这种形式,因此需要自定义key的规则。import java.lang.annotation.*; @Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheParam { String name() default ""; }3.定义key生成策略接口import org.aspectj.lang.ProceedingJoinPoint; import org.springframework.stereotype.Service; public interface CacheKeyGenerator { //获取AOP参数,生成指定缓存Key String getLockKey(ProceedingJoinPoint joinPoint); }4.定义key生成策略实现类package com.example.demo.service.impl; import com.example.demo.service.CacheKeyGenerator; import com.example.demo.utils.CacheLock; import com.example.demo.utils.CacheParam; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; public class CacheKeyGeneratorImp implements CacheKeyGenerator { @Override public String getLockKey(ProceedingJoinPoint joinPoint) { //获取连接点的方法签名对象 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); //Method对象 Method method = methodSignature.getMethod(); //获取Method对象上的注解对象 CacheLock cacheLock = method.getAnnotation(CacheLock.class); //获取方法参数 final Object[] args = joinPoint.getArgs(); //获取Method对象上所有的注解 final Parameter[] parameters = method.getParameters(); StringBuilder sb = new StringBuilder(); for(int i=0;i<parameters.length;i++){ final CacheParam cacheParams = parameters[i].getAnnotation(CacheParam.class); //如果属性不是CacheParam注解,则不处理 if(cacheParams == null){ continue; } //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam sb.append(cacheLock.delimiter()).append(args[i]); } //如果方法上没有加CacheParam注解 if(StringUtils.isEmpty(sb.toString())){ //获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组) final Annotation[][] parameterAnnotations = method.getParameterAnnotations(); //循环注解 for(int i=0;i<parameterAnnotations.length;i++){ final Object object = args[i]; //获取注解类中所有的属性字段 final Field[] fields = object.getClass().getDeclaredFields(); for(Field field : fields){ //判断字段上是否有CacheParam注解 final CacheParam annotation = field.getAnnotation(CacheParam.class); //如果没有,跳过 if(annotation ==null){ continue; } //如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量) field.setAccessible(true); //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam sb.append(cacheLock.delimiter()).append(ReflectionUtils.getField(field,object)); } } } //返回指定前缀的key return cacheLock.prefix() + sb.toString(); } }5.分布式注解实现import com.example.demo.service.CacheKeyGenerator; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.util.StringUtils; import java.lang.reflect.Method; @Aspect @Configuration public class CacheLockMethodInterceptor { @Autowired public CacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){ this.cacheKeyGenerator = cacheKeyGenerator; this.stringRedisTemplate = stringRedisTemplate; } private final StringRedisTemplate stringRedisTemplate; private final CacheKeyGenerator cacheKeyGenerator; @Around("execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)") public Object interceptor(ProceedingJoinPoint joinPoint){ MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); CacheLock cacheLock = method.getAnnotation(CacheLock.class); if(StringUtils.isEmpty(cacheLock.prefix())){ throw new RuntimeException("前缀不能为空"); } //获取自定义key final String lockkey = cacheKeyGenerator.getLockKey(joinPoint); final Boolean success = stringRedisTemplate.execute( (RedisCallback<Boolean>) connection -> connection.set(lockkey.getBytes(), new byte[0], Expiration.from(cacheLock.expire(), cacheLock.timeUnit()) , RedisStringCommands.SetOption.SET_IF_ABSENT)); if (!success) { // TODO 按理来说 我们应该抛出一个自定义的 CacheLockException 异常;这里偷下懒 throw new RuntimeException("请勿重复请求"); } try { return joinPoint.proceed(); } catch (Throwable throwable) { throw new RuntimeException("系统异常"); } } }6.主函数调整 @Bean public CacheKeyGenerator cacheKeyGenerator(){ return new CacheKeyGeneratorImp(); }7.Controller@ResponseBody @PostMapping(value ="/cacheLock") @ApiOperation(value="重复提交验证测试--使用redis锁") @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")}) //@CacheLock @CacheLock() public String cacheLock(String token){ return "sucess====="+token; } @ResponseBody @PostMapping(value ="/cacheLock1") @ApiOperation(value="重复提交验证测试--使用redis锁") @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")}) //@CacheLock @CacheLock(prefix = "redisLock.test",expire = 20) public String cacheLock1(String token){ return "sucess====="+token; } @ResponseBody @PostMapping(value ="/cacheLock2") @ApiOperation(value="重复提交验证测试--使用redis锁") @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")}) //@CacheLock @CacheLock(prefix = "redisLock.test",expire = 20) public String cacheLock2(@CacheParam(name = "token") String token){ return "sucess====="+token; }
2021年03月08日
84 阅读
0 评论
0 点赞
2021-03-08
SpringBoot应用中使用AOP记录接口访问日志
SpringBoot应用中使用AOP记录接口访问日志
2021年03月08日
211 阅读
1 评论
0 点赞
2021-03-05
使用Dockerfile为SpringBoot应用构建Docker镜像
通过docker-maven-plugin来构建docker镜像的方式,此种方式需要依赖自建的Registry镜像仓库。本文将讲述另一种方式,使用Dockerfile来构建docker镜像,此种方式不需要依赖自建的镜像仓库,只需要应用的jar包和一个Dockerfile文件即可。1.Dockerfile常用指令ADD用于复制文件,格式:ADD 示例:# 将当前目录下的mall-tiny-docker-file.jar包复制到docker容器的/目录下 ADD mall-tiny-docker-file.jar /mall-tiny-docker-file.jarENTRYPOINT指定docker容器启动时执行的命令,格式:ENTRYPOINT ["executable", "param1","param2"...]示例:# 指定docker容器启动时运行jar包 ENTRYPOINT ["java", "-jar","/mall-tiny-docker-file.jar"]ENV用于设置环境变量,格式:ENV 示例:# mysql运行时设置root密码 ENV MYSQL_ROOT_PASSWORD rootEXPOSE声明需要暴露的端口(只声明不会打开端口),格式:EXPOSE ...示例:# 声明服务运行在8080端口 EXPOSE 8080FROM指定所需依赖的基础镜像,格式:FROM :示例:# 该镜像需要依赖的java8的镜像 FROM java:8MAINTAINER指定维护者的名字,格式:MAINTAINER 示例:MAINTAINER bdysoftRUN在容器构建过程中执行的命令,我们可以用该命令自定义容器的行为,比如安装一些软件,创建一些文件等,格式:RUN RUN ["executable", "param1","param2"...]示例:# 在容器构建过程中需要在/目录下创建一个mall-tiny-docker-file.jar文件 RUN bash -c 'touch /mall-tiny-docker-file.jar'2.使用Dockerfile构建SpringBoot应用镜像编写Dockerfile文件# 该镜像需要依赖的基础镜像 FROM java:8 # 将当前目录下的jar包复制到docker容器的/目录下 ADD mall-tiny-docker-file-0.0.1-SNAPSHOT.jar /mall-tiny-docker-file.jar # 运行过程中创建一个mall-tiny-docker-file.jar文件 RUN bash -c 'touch /mall-tiny-docker-file.jar' # 声明服务运行在8080端口 EXPOSE 8080 # 指定docker容器启动时运行jar包 ENTRYPOINT ["java", "-jar","/mall-tiny-docker-file.jar"] # 指定维护者的名字 MAINTAINER macrozheng3.使用maven打包应用在IDEA中双击package命令进行打包将应用jar包及Dockerfile文件上传到linux服务器4.在Linux上构建docker镜像在Dockerfile所在目录执行以下命令# -t 表示指定镜像仓库名称/镜像名称:镜像标签 .表示使用当前目录下的Dockerfile docker build -t mall-tiny/mall-tiny-docker-file:0.0.1-SNAPSHOT .
2021年03月05日
89 阅读
0 评论
0 点赞
2021-03-05
使用Docker Compose部署SpringBoot应用
Docker Compose是一个用于定义和运行多个docker容器应用的工具。使用Compose你可以用YAML文件来配置你的应用服务,然后使用一个命令,你就可以部署你配置的所有服务了。一、安装1.下载Docker Composecurl -L https://get.daocloud.io/docker/compose/releases/download/1.24.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose2.修改该文件的权限为可执行chmod +x /usr/local/bin/docker-compose3.查看是否已经安装成功docker-compose --version二、使用Docker Compose的步骤使用Dockerfile定义应用程序环境,一般需要修改初始镜像行为时才需要使用;使用docker-compose.yml定义需要部署的应用程序服务,以便执行脚本一次性部署;使用docker-compose up命令将所有应用服务一次性部署起来。2.1docker-compose.yml常用命令image指定运行的镜像名称# 运行的是mysql5.7的镜像 image: mysql:5.7container_name配置容器名称# 容器名称为mysql container_name: mysqlports指定宿主机和容器的端口映射(HOST:CONTAINER)# 将宿主机的3306端口映射到容器的3306端口 ports: - 3306:3306volumes将宿主机的文件或目录挂载到容器中(HOST:CONTAINER)# 将外部文件挂载到myql容器中 volumes: - /mydata/mysql/log:/var/log/mysql - /mydata/mysql/data:/var/lib/mysql - /mydata/mysql/conf:/etc/mysqlenvironment配置环境变量# 设置mysqlroot帐号密码的环境变量 environment: - MYSQL_ROOT_PASSWORD=rootlinks连接其他容器的服务(SERVICE:ALIAS)# 可以以database为域名访问服务名称为db的容器 links: - db:database三、Docker Compose常用命令构建、创建、启动相关容器# -d表示在后台运行 docker-compose up -d指定文件启动docker-compose -f docker-compose.yml up -d停止所有相关容器docker-compose stop列出所有容器信息docker-compose ps四、使用Docker Compose 部署应用3.1编写docker-compose.yml文件Docker Compose将所管理的容器分为三层,工程、服务及容器。docker-compose.yml中定义所有服务组成了一个工程,services节点下即为服务,服务之下为容器。容器与容器直之间可以以服务名称为域名进行访问,比如在mall-tiny-docker-compose服务中可以通过jdbcmysql//db:3306这个地址来访问db这个mysql服务。version: '3' services: # 指定服务名称 db: # 指定服务使用的镜像 image: mysql:5.7 # 指定容器名称 container_name: mysql # 指定服务运行的端口 ports: - 3306:3306 # 指定容器中需要挂载的文件 volumes: - /mydata/mysql/log:/var/log/mysql - /mydata/mysql/data:/var/lib/mysql - /mydata/mysql/conf:/etc/mysql # 指定容器的环境变量 environment: - MYSQL_ROOT_PASSWORD=root # 指定服务名称 mall-tiny-docker-compose: # 指定服务使用的镜像 image: mall-tiny/mall-tiny-docker-compose:0.0.1-SNAPSHOT # 指定容器名称 container_name: mall-tiny-docker-compose # 指定服务运行的端口 ports: - 8080:8080 # 指定容器中需要挂载的文件 volumes: - /etc/localtime:/etc/localtime - /mydata/app/mall-tiny-docker-compose/logs:/var/logs注意:如果遇到mall-tiny-docker-compose服务无法连接到mysql,需要在mysql中建立mall数据库,同时导入mall.sql脚本。具体参考使用Dockerfile为SpringBoot应用构建Docker镜像中的运行mysql服务并设置部分。五、使用maven插件构建mall-tiny-docker-compose镜像六、运行Docker Compose命令启动所有服务先将docker-compose.yml上传至Linux服务器,再在当前目录下运行如下命令:docker-compose up -d
2021年03月05日
97 阅读
0 评论
0 点赞
2021-03-05
使用Seata彻底解决Spring Cloud中的分布式事务问题!
Seata是Alibaba开源的一款分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,本文将通过一个简单的下单业务场景来对其用法进行详细介绍。一、什么是分布式事务问题?1.1单体应用单体应用中,一个业务操作需要调用三个模块完成,此时数据的一致性由本地事务来保证。1.2微服务应用 随着业务需求的变化,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。1.3小结在微服务架构中由于全局数据一致性没法保证产生的问题就是分布式事务问题。简单来说,一次业务操作需要操作多个数据源或需要进行远程调用,就会产生分布式事务问题。1.4Seata简介 Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。1.5Seata原理和设计定义一个分布式事务 我们可以把一个分布式事务理解成一个包含了若干分支事务的全局事务,全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足ACID的本地事务。这是我们对分布式事务结构的基本认识,与 XA 是一致的。协议分布式事务处理过程的三个组件Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。一个典型的分布式事务过程TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;XID 在微服务调用链路的上下文中传播;RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;TM 向 TC 发起针对 XID 的全局提交或回滚决议;TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。二、seata-server的安装与配置我们先从官网下载seata-server,这里下载的是seata-server-0.9.0.zip,下载地址:https://github.com/seata/seata/releases这里我们使用Nacos作为注册中心,Nacos的安装及使用可以参考:Spring Cloud Alibaba:Nacos 作为注册中心和配置中心使用;解压seata-server安装包到指定目录,修改conf目录下的file.conf配置文件,主要修改自定义事务组名称,事务日志存储模式为db及数据库连接信息;service { #vgroup->rgroup vgroup_mapping.fsp_tx_group = "default" #修改事务组名称为:fsp_tx_group,和客户端自定义的名称对应 #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } ## transaction log store store { ## store mode: file、db mode = "db" #修改此处将事务信息存储到数据库中 ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://localhost:3306/seat-server" #修改数据库连接地址 user = "root" #修改数据库用户名 password = "root" #修改数据库密码 min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } }由于我们使用了db模式存储事务日志,所以我们需要创建一个seat-server数据库,建表sql在seata-server的/conf/db_store.sql中;修改conf目录下的registry.conf配置文件,指明注册中心为nacos,及修改nacos连接信息即可;registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" #改为nacos nacos { serverAddr = "localhost:8848" #改为nacos的连接地址 namespace = "" cluster = "default" } }先启动Nacos,再使用seata-server中/bin/seata-server.bat文件启动seata-server。三、数据库准备3.1创建业务数据库seat-order:存储订单的数据库;seat-storage:存储库存的数据库;seat-account:存储账户信息的数据库。初始化业务表order表CREATE TABLE `order` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `user_id` bigint(11) DEFAULT NULL COMMENT '用户id', `product_id` bigint(11) DEFAULT NULL COMMENT '产品id', `count` int(11) DEFAULT NULL COMMENT '数量', `money` decimal(11,0) DEFAULT NULL COMMENT '金额', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' AFTER `money` ;storage表CREATE TABLE `storage` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `product_id` bigint(11) DEFAULT NULL COMMENT '产品id', `total` int(11) DEFAULT NULL COMMENT '总库存', `used` int(11) DEFAULT NULL COMMENT '已用库存', `residue` int(11) DEFAULT NULL COMMENT '剩余库存', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `seat-storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');account表CREATE TABLE `account` ( `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `user_id` bigint(11) DEFAULT NULL COMMENT '用户id', `total` decimal(10,0) DEFAULT NULL COMMENT '总额度', `used` decimal(10,0) DEFAULT NULL COMMENT '已用余额', `residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `seat-account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');3.2创建日志回滚表使用Seata还需要在每个数据库中创建日志表,建表sql在seata-server的/conf/db_undo_log.sql中。3.3完整数据库示意图四、制造一个分布式事务问题这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。4.1客户端配置对seata-order-service、seata-storage-service和seata-account-service三个seata的客户端进行配置,它们配置大致相同,我们下面以seata-order-service的配置为例;修改application.yml文件,自定义事务组的名称;spring: cloud: alibaba: seata: tx-service-group: fsp_tx_group #自定义事务组名称需要与seata-server中的对应添加并修改file.conf配置文件,主要是修改自定义事务组名称;service { #vgroup->rgroup vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称 #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" disableGlobalTransaction = false } 添加并修改registry.conf配置文件,主要是将注册中心改为nacos;registry { # file 、nacos 、eureka、redis、zk type = "nacos" #修改为nacos nacos { serverAddr = "localhost:8848" #修改为nacos的连接地址 namespace = "" cluster = "default" } }在启动类中取消数据源的自动创建:@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableDiscoveryClient @EnableFeignClients public class SeataOrderServiceApplication { public static void main(String[] args) { SpringApplication.run(SeataOrderServiceApplication.class, args); } }创建配置使用Seata对数据源进行代理:/** * 使用Seata对数据源进行代理 */ @Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }使用@GlobalTransactional注解开启分布式事务:package com.macro.cloud.service.impl; import com.macro.cloud.dao.OrderDao; import com.macro.cloud.domain.Order; import com.macro.cloud.service.AccountService; import com.macro.cloud.service.OrderService; import com.macro.cloud.service.StorageService; import io.seata.spring.annotation.GlobalTransactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * 订单业务实现类 */ @Service public class OrderServiceImpl implements OrderService { private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class); @Autowired private OrderDao orderDao; @Autowired private StorageService storageService; @Autowired private AccountService accountService; /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 */ @Override @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) public void create(Order order) { LOGGER.info("------->下单开始"); //本应用创建订单 orderDao.create(order); //远程调用库存服务扣减库存 LOGGER.info("------->order-service中扣减库存开始"); storageService.decrease(order.getProductId(),order.getCount()); LOGGER.info("------->order-service中扣减库存结束:{}",order.getId()); //远程调用账户服务扣减余额 LOGGER.info("------->order-service中扣减余额开始"); accountService.decrease(order.getUserId(),order.getMoney()); LOGGER.info("------->order-service中扣减余额结束"); //修改订单状态为已完成 LOGGER.info("------->order-service中修改订单状态开始"); orderDao.update(order.getUserId(),0); LOGGER.info("------->order-service中修改订单状态结束"); LOGGER.info("------->下单结束"); } }五、分布式事务功能演示运行seata-order-service、seata-storage-service和seata-account-service三个服务;数据库初始信息状态:调用接口进行下单操作后查看数据库:http://localhost:8180/order/create?userId=1&productId=1&count=10&money=100我们在seata-account-service中制造一个超时异常后,调用下单接口:/** * 账户业务实现类 */ @Service public class AccountServiceImpl implements AccountService { private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class); @Autowired private AccountDao accountDao; /** * 扣减账户余额 */ @Override public void decrease(Long userId, BigDecimal money) { LOGGER.info("------->account-service中扣减账户余额开始"); //模拟超时异常,全局事务回滚 try { Thread.sleep(30*1000); } catch (InterruptedException e) { e.printStackTrace(); } accountDao.decrease(userId,money); LOGGER.info("------->account-service中扣减账户余额结束"); } }此时我们可以发现下单后数据库数据并没有任何改变;我们可以在seata-order-service中注释掉@GlobalTransactional来看看没有Seata的分布式事务管理会发生什么情况:/** * 订单业务实现类 */ @Service public class OrderServiceImpl implements OrderService { /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 */ @Override // @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) public void create(Order order) { LOGGER.info("------->下单开始"); //省略代码... LOGGER.info("------->下单结束"); } }由于seata-account-service的超时会导致当库存和账户金额扣减后订单状态并没有设置为已经完成,而且由于远程调用的重试机制,账户余额还会被多次扣减。
2021年03月05日
129 阅读
0 评论
0 点赞
2021-03-05
SpringBoot整合SpringTask实现定时任务
SpringTask是Spring自主研发的轻量级定时任务工具,相比于Quartz更加简单方便,且不需要引入其他依赖即可使用。Cron表达式是一个字符串,包括6~7个时间元素,在SpringTask中可以用于指定任务的执行时间。在线生成Cron表达式业务场景说明:用户对某商品进行下单操作,系统需要根据用户购买的商品信息生成订单并锁定商品的库存,系统设置了60分钟用户不付款就会取消订单,开启一个定时任务,每隔10分钟检查下,如果有超时还未付款的订单,就取消订单并取消锁定的商品库存。当然我们也可以用MQ实现延迟消息的方式来实现超时取消订单。1.在配置类中添加一个@EnableScheduling注解即可开启SpringTask的定时任务能力mport org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; /** * 定时任务配置 */ @Configuration @EnableScheduling public class SpringTaskConfig { }2.添加OrderTimeOutCancelTask来执行定时任务import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** * 订单超时取消并解锁库存的定时器 */ @Component public class OrderTimeOutCancelTask { private Logger LOGGER = LoggerFactory.getLogger(OrderTimeOutCancelTask.class); /** * cron表达式:Seconds Minutes Hours DayofMonth Month DayofWeek [Year] * 每10分钟扫描一次,扫描设定超时时间之前下的订单,如果没支付则取消该订单 */ @Scheduled(cron = "0 0/10 * ? * ?") private void cancelTimeOutOrder() { // 此处调用取消订单的方法, LOGGER.info("取消订单,并根据sku编号释放锁定库存"); } }
2021年03月05日
130 阅读
0 评论
0 点赞
2021-03-05
JAVA中通过Hibernate-Validation进行参数验证
在开发JAVA服务器端代码时,我们会遇到对外部传来的参数合法性进行验证,而hibernate-validator提供了一些常用的参数校验注解,我们可以拿来使用。1.引入依赖<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.3.1.Final</version> </dependency>2.在Model中定义要校验的字段import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.NotEmpty; @Data public class PayRequestDto { /** * 支付完成时间 **/ @NotEmpty(message="支付完成时间不能空") @Size(max=14,message="支付完成时间长度不能超过{max}位") private String payTime; /** * 状态 **/ @Pattern(regexp = "0[0123]", message = "状态只能为00或01或02或03") private String status; }3.定义Validation工具类import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import org.hibernate.validator.HibernateValidator; import com.atai.framework.lang.AppException; public class ValidationUtils { /** * 使用hibernate的注解来进行验证 * */ private static Validator validator = Validation .byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator(); /** * 功能描述: <br> * 〈注解验证参数〉 * * @param obj * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */ public static <T> void validate(T obj) { Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj); // 抛出检验异常 if (constraintViolations.size() > 0) { throw new AppException("0001", String.format("参数校验失败:%s", constraintViolations.iterator().next().getMessage())); } } }4.在代码中调用工具类进行参数校验ValidationUtils.validate(requestDto);以下是对hibernate-validator中部分注解进行描述:注解作用@AssertTrue用于boolean字段,该字段只能为true@AssertFalse该字段的值只能为false@CreditCardNumber对信用卡号进行一个大致的验证@DecimalMax只能小于或等于该值@DecimalMin只能大于或等于该值@Digits(integer=,fraction=)检查是否是一种数字的整数、分数,小数位数的数字@Email检查是否是一个有效的email地址@Future检查该字段的日期是否是属于将来的日期@Length(min=,max=)检查所属的字段的长度是否在min和max之间,只能用于字符串@Max该字段的值只能小于或等于该值@Min该字段的值只能大于或等于该值@NotNull不能为null@NotBlank不能为空,检查时会将空格忽略@NotEmpty不能为空,这里的空是指空字符串@Null检查该字段为空@Past检查该字段的日期是在过去@Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式Range(min=,max=,message=)被注释的元素必须在合适的范围内@Size(min=, max=)检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map等@URL(protocol=,host,port)检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件@Valid该注解主要用于字段为一个包含其他对象的集合或map或数组的字段,或该字段直接为一个其他对象的引用,这样在检查当前对象的同时也会检查该字段所引用的对象
2021年03月05日
128 阅读
0 评论
0 点赞
2021-03-05
超强大的XML和JavaBean相互转换的工具类
话不多说,直接上代码import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringWriter; import java.text.MessageFormat; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamWriter; /** * @Description:xml字符串转换Javabean * @author:lgh * @date: 2018-11-20 10:55 */ public class JaxbUtils { @SuppressWarnings("unchecked") private static <T> T readString(Class<T> clazz, String context) throws JAXBException { try { JAXBContext jc = JAXBContext.newInstance(clazz); Unmarshaller u = jc.createUnmarshaller(); return (T) u.unmarshal(new File(context)); } catch (JAXBException e) { throw e; } } @SuppressWarnings("unchecked") private static <T> T readConfig(Class<T> clazz, String config, Object... arguments) throws IOException, JAXBException { InputStream is = null; try { if (arguments.length > 0) { config = MessageFormat.format(config, arguments); } JAXBContext jc = JAXBContext.newInstance(clazz); Unmarshaller u = jc.createUnmarshaller(); is = new FileInputStream(config); return (T) u.unmarshal(is); } catch (IOException e) { throw e; } catch (JAXBException e) { throw e; } finally { if (is != null) { is.close(); } } } @SuppressWarnings("unchecked") private static <T> T readConfigFromStream(Class<T> clazz, InputStream dataStream) throws JAXBException { try { JAXBContext jc = JAXBContext.newInstance(clazz); Unmarshaller u = jc.createUnmarshaller(); return (T) u.unmarshal(dataStream); } catch (JAXBException e) { // logger.trace(e); throw e; } } @SuppressWarnings("unchecked") private static <T> T readConfigFromRead(Class<T> clazz, Reader reader)throws JAXBException { try { JAXBContext jc = JAXBContext.newInstance(clazz); Unmarshaller u = jc.createUnmarshaller(); return (T) u.unmarshal(reader); } catch (JAXBException e) { // logger.trace(e); throw e; } } /** * xml转换成java bean * @param xml * @param clazz * @return * @throws JAXBException */ public static <T> T jaxbConvertXmlToBean(String xml,Class<T> clazz) { InputStream is = null; try { is = new ByteArrayInputStream(xml.getBytes("UTF-8")); } catch (Exception e) { e.printStackTrace(); }finally{ if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } T t = null; try { t = JaxbUtils.readConfigFromStream(clazz, is); } catch (JAXBException e) { e.printStackTrace(); } return t; } /** * java bean 转换成xml * @param object * @return * @throws JAXBException */ public static String jaxbConvertBeanToXml(Object object)throws JAXBException { StringWriter writer = new StringWriter(); String returnString =""; try { JAXBContext jc = JAXBContext.newInstance(object.getClass()); Marshaller m = jc.createMarshaller(); m.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); m.marshal(object, writer); returnString=writer.toString(); } catch (Exception e) { e.printStackTrace(); }finally{ if(writer!=null){ try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } return returnString; } public static String jaxbBeanToxml(Object object){ try { JAXBContext jaxbContext = JAXBContext.newInstance(object.getClass()); Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); jaxbMarshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); jaxbMarshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); ByteArrayOutputStream baos = new ByteArrayOutputStream(); XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(baos, (String) jaxbMarshaller.getProperty(Marshaller.JAXB_ENCODING)); xmlStreamWriter.writeStartDocument((String) jaxbMarshaller.getProperty(Marshaller.JAXB_ENCODING), "1.0"); jaxbMarshaller.marshal(object, xmlStreamWriter); xmlStreamWriter.writeEndDocument(); xmlStreamWriter.close(); return new String(baos.toByteArray(),"UTF-8"); } catch (Exception e) { } return null; } public static String jaxbBeanToxmlGBK(Object object){ try { JAXBContext jaxbContext = JAXBContext.newInstance(object.getClass()); Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); jaxbMarshaller.setProperty(Marshaller.JAXB_ENCODING, "GBK"); jaxbMarshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); ByteArrayOutputStream baos = new ByteArrayOutputStream(); XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(baos, (String) jaxbMarshaller.getProperty(Marshaller.JAXB_ENCODING)); xmlStreamWriter.writeStartDocument((String) jaxbMarshaller.getProperty(Marshaller.JAXB_ENCODING), "1.0"); jaxbMarshaller.marshal(object, xmlStreamWriter); xmlStreamWriter.writeEndDocument(); xmlStreamWriter.close(); return new String(baos.toByteArray(),"GBK"); } catch (Exception e) { e.printStackTrace(); } return null; } /** * xml转换成java bean * @param xml * @param clazz * @return * @throws JAXBException */ public static <T> T jaxbConvertXmlToBeanByGbk(String xml,Class<T> clazz) { InputStream is = null; try { is = new ByteArrayInputStream(xml.getBytes("GBK")); } catch (Exception e) { e.printStackTrace(); }finally{ if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } T t = null; try { t = JaxbUtils.readConfigFromStream(clazz, is); } catch (JAXBException e) { e.printStackTrace(); } return t; } }针对上述工具类,给记得给JavaBean加上相应注解如下:@XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "TransType") @Data public class TransType { @XmlElement(name = "BaseInfo",required = true) protected BaseInfo baseInfo; @XmlElement(name = "OutputData",required = true) protected OutputData outputData; }
2021年03月05日
155 阅读
0 评论
0 点赞
2021-03-05
Centos7 实现端口转发(rinetd实现)
# 如果没有安装gcc 先安装gcc,这里就全部一并安装了 yum -y install gcc+ gcc-c++ yum -y install make #安装,如果地址失效,去官网查看下载地址即可 wget https://boutell.com/rinetd/http/rinetd.tar.gz tar -zxf rinetd.tar.gz mkdir -p /usr/man/man8 make && make install # 配置 vim /etc/rinetd.conf # 一行即为一条转发规则 格式如:[source_address] [source_port] [destination_address] [destination_port] #[本机IP(若非多网卡直接设为0.0.0.0)] [转发端口] [服务IP] [服务端口] #我本次转发的端口是hbase的即为 192.168.9.87 16000 172.17.0.2 16000 192.168.9.87 16010 172.17.0.2 16010 #启动 rinetd -c /etc/rinetd.conf #停止 直接kill 即可 killall -9 rinetd #查看转发 netstat -tanulp|grep rinetd
2021年03月05日
192 阅读
0 评论
0 点赞
2021-03-04
经年花开陌上离
{mtitle title="1"/} 初见她的时候,是在网吧的一角,周遭是几个满身烟酒味的青年,她立在中间,贪婪地吸着烟,沉醉在那腾云驾雾的幻觉中,全然不知一旁的我,默然相望,手指敲打在键盘边的烟灰缸上,生生刺痛。 如果没有那一次的交集,或许我们之间不会有任何故事,或许我们只是同一空间但却不是同一世界的人,就像两条平行线,无限蔓延到看不见。 可是,当铭和你牵手出现在我面前的时候,我知道我们已经有了交点,你是我无法逃离的命中注定。我很自私地问下你的手机号,我知道铭是不会介意的。 他说,驰,我们是很好很好的兄弟,如果喜欢,就说一声。他是在一次酒醉后对我说的,昏暗的灯光下,我看到他扭曲的脸想到了自己被扭曲的心灵。我笑着回应他怎么可能,我怎么可能会喜欢柔若呢。 送他回家的时候,我看到不远处的柔若,一脸的焦虑,她说,铭,我终于找到你了,快回家吧!然后转身对我笑笑,驰,谢谢你,真不愧是铭的好兄弟。 只是一脸无所谓的笑意,转身的刹那脸上挂满了透明的滴状液体,柔若,你知道吗?我是故意把他灌醉,然后送他回家,只想能够遇见你。 有些事,是不想做却不得不做的,有些话,是想说却无法言语的。我知道对你的爱恋,是无可言说的秘密。 睡了吗?铭没事了吧?按下发送键,手机在掌心翻转,干净而又利落,心却像打翻了五味瓶。我们之间的谈话,总是围绕铭展开的。 手机一直静止在掌心,或许是都睡下了吧,我一边自我安慰着一边关掉手机,在黑夜中摸索着前进,然后回到家里,再一次用酒精麻痹自己,我知道我无法忘记。 当铭笑着告诉我你才15岁的时候,柔若,你知道那瞬间我内心闪过的怜惜吗?我不想相信,我宁愿你有25岁,即使苍老,也无所惜。 我说,柔若,你还那么小,跟铭一起会幸福吗?还是回去好好念书吧! 我不喜欢读书,又没有地方去啊,只有和铭一起。说完无奈地吐吐舌头扮一个可爱的鬼脸,像个孩子,让我有一种怜惜的感觉。 15岁,正值花季,却被社会无情地糟蹋,是她不懂珍惜吗?还是她自甘堕落,看着她欢快地跳跃在我们面前,我停止了往下想,那么可爱的人,无法把她与堕落联系。 我总是在他们一起的时候,用玩笑的口吻说,铭,要是早知道不读书也能像你那样找到那么好的女朋友,打死我也要在社会上鬼混啊。之后是无休止的大笑,笑到泪水慢慢的流下,直到铭用力拍打我的胸口说,你小子没事吧。 我想说我没事,我只是想让柔若知道,她很好。可是有些话早已不能对她说,就像那份爱,仅是心想而已。 彼岸盛开的花朵在瞬间娇艳后以光速凋谢,我以为只要有旷世的神速便可以追及,只是我不知道,在我开始的时候花已凋谢。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="2"/} 彼时她已和铭分手,而我正在准备着即将到来的中考。 已经是深夜,睡梦中被刺耳的铃声吵醒,那是她第一次给我打来电话,电话的那头,是她带着哭腔的声音。 昏暗的夜色包杂着太多的沉郁,来不及逃离。 她说,为什么那么假?他怎么可以玩弄我的感情?难道这个世界上真的连老子都是假的? 我慌然失措,我是不善安慰人的,只是轻轻地说,该发生的终会发生,令人心碎的声音,无限怜惜。忘了是以什么方式结束了我们的谈话,只是脑海中不时闪过零碎的哭泣声,一向乐观的女咳,那一夜却哭得泪眼滂沱。我知道我亦是伤心的,我的喜怒哀乐早已受制于她,不属于我一个人。 一个上午都被昨夜心碎的哭泣声扰乱着自己的心绪,迷迷糊糊地睡去,睡梦中是两个牵手的背影,一地菊花,很是浪漫,印象还越发清晰的时候才伤心地发现,两个人不代表我们。 “如果和你牵手的无法是我,我希望和我牵手的是你,即使你掌心握的是一个又一个男子的手,而我的手只等你来牵。我只想牵你的手,给你一个干净温暖的怀抱。” 她说她看到这条留言的时候笑了,然后又认真地告诉我,不要相信爱情。这个世界就连老子都是假的,太过于执着的追求只会重伤到自己。人有时候要懂得放弃。 再见到她的时候,身边已换了另一个男子,五颜六色的长发下青春叛逆的气息,口中吸着半枝香烟搂着弱小的她出现在我面前的时候我差一点认不出来,她已变了很多,不再是那天真的装束,鲜艳的装束下是一张幼稚的脸蛋。 我说,柔若,他就是你男朋友啊?我知道我是明知故问,可是我无法面对现实。 柔若看了看我,错愕了几秒后,然后使劲地点头,不停地说是啊是啊。 我想我什么都听不见却还是什么都已听见,我开始憎恶她身边的青年,一个不思上进,无所事事的混混为什么可以轻而易举地获取我喜欢的人的心,而我却只能远远地看着你们?完美的爱情神话直到落幕我仍旧是一个什么也无法改变的局外人。对于戏中早已固定的画面我无力改变。 她说,小驰,忘了我吧!我们不是同一个世界的人,注定无法在一起。 是吗?注定?我苦笑,如果真有注定,那我们的相遇,是否是注定的悲伤? 我没有说话,我知道有些事是无法勉强的,比如爱情,可以建立在钱的基础上,却无法用情把两个人联系到一起。天空在经历了一阵大哭之后心情似乎开朗了很多,淡蓝色的固定在头顶的上空,远处的阳光刺眼地照射着它脚下的大地,悲伤如阳光般弥漫整个空间。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="3"/} 若是无缘,擦身亦是无缘会见。涛声依旧,悦耳灵笛空为谁赋? 她说,我是一个经历过不堪之事的女人,我们注定不是同一类人。 我说我知道,一开始我就知道,你是一个有着不愉快回忆的人,你用心隐藏着过去,用堕落麻痹自己,让自己过着若无其事的生活。 可是,柔若,你知道吗?我错过了你的童年,年少的交集却是印证悲剧的发生,我只想给你一个干净温暖的怀抱,让你过上平静的生活,却也难以实现,我知道童年的错过是我无法弥补的遗憾,不想再眼睁睁地错过你的少年。她说,我总会不停的想起童年的那片血腥,暗红色的血液涂满天际。那时的你亲眼看着自己的亲人一个个倒在血泊里,内心无法承载太多的伤痛而被痛昏去。 她亦没有继续说下去,我知道,她是没有勇气,也不愿再重提。 可是,是否所以的不幸,都可以成为堕落的借口? 中考已然过去,我无奈地看着自己少得可怜的成绩独自哀叹,突然想起柔若的一句话,她说,小驰你知道吗?我们是这个世界上最卑微的人,我们卑微到什么也不是,那些美好的东西不会像不幸那样常常光顾我们,即使我们用尽一生的力量去努力追寻,亦是没有收获。 幼稚的脸蛋,却异常的少年老成,如果说一个历经沙场在死亡边缘游走过的人在离开沙场后都成了哲学家和诗人,是因为他们感悟太多的话,那么柔若的少年老成应该是经历过太多太多的苦难了吧! 当柔若再一次躺在家里给我打电话的时候,我感到呼吸的压抑。 她说,驰,你知道吗?铭说他高中毕业不上大学了,是因为一个女子,为什么他还要伤害我?明知道不会和我一起为什么要让我和他曾经在一起。 她说,铭等的女子已经出现了,而她的幸福已经破碎,散落一地。 只是听着,直到抽泣声渐弱,我说,柔若,何苦呢?早已过去,你也换了好几任男友。铭是不属于你的,就像你不属于我一样。 她说,你不会明白,驰,你永远不会明白,铭是我第一次爱上也是我这辈子唯一爱上的。那种失去的疼痛像针一样,刺痛我的心。 柔若,我是明白的,就像你是我此生唯一的仰望一样,铭是你的整个夜空,而我只是你夜空中一闪而过的流星,在别人的许愿声中消逝,用自己的牺牲换取别人的幸福。我说,柔若,我会想办法让铭回到你身边。 凄凉的笑意,柔若的声音变得越发虚弱。她说,驰,你听过心碎的声音吗?跟血滴在地板上的声音一样。我现在,听到了心碎的声音。 我知道某些不好的事情已经发生,发了疯一般地冲向柔若的住所,我知道,如果鲜花将要凋谢,是没有人能够阻止的,可我不想她消失在我的视线。 透过玻璃窗,是柔若倚在铭的肩上,地板上一片腥红,但柔若显然已被包扎过。铭在不经意间看到窗外的我,一把推开柔若,径直向我走来。 驰,你好好地去安慰她吧,我走了。亦不再回来。然后转过头对着满脸泪珠的柔若说,下次自杀的时候,别忘了关好门窗,转身离开,再度用无声的泪为伤痛撕开一个出口。 我看到柔若的脸上闪过刹那的沉痛,心像散了一地的破碎的玻璃,亦如平静的湖面漾起阵阵涟漪。我是知道铭的,不会恰巧在她自杀时从柔若的家门前经过。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="4"/} 雨,沥沥晰晰地数落着,好象情人的眼泪! 铭就这样与落雨融为一体。泪水蔓延,与雨融合成了一线。铭在哭泣。然而,没有人可以感觉到,除了自己。 即使,把所有的想象的翅膀都插入了铭的脊骨里,他也无法意识到最后的结局竟是如此痛彻心扉。 站在熟悉的道上,望着熟悉的景致,记忆的流水在铭的心头连续不断地涌着 那时候的自己,还是一个高中生吧,与柔若的认识是只是一场意外,就像海鸟跟鱼的相遇,很简单。然而,这样简单的相遇却寓言着一场刻骨铭心的爱会非比寻常,意料之外的相遇,却是意料之内的伤害。 海鸟跟鱼相遇,只是一场意外。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="5"/} 如果我可以遇见你的童年,我一定可以给你平安的一生,奈何我错过了你的童年,你已成了一个有故事的人,承载了太多,已容不下我进驻你的心。 彼时她已经16岁,生日那天,是铭打电话提醒我要为她过生日。铭依旧用冷漠的语气,语意中透出一丝凉意。我知道,一个人外表的冷漠只是为了掩饰内心的脆弱。 她说,驰,过完我的16岁生日,我就要去我妈妈那了,那样,或许可以忘了铭。 我忙说好啊好啊,那样你就可以过回正常的生活了。也许是酒吧的光线太过于昏暗,我竟没有看到她眼中划过的泪滴。 她不停地往自己的酒杯里倒酒,一杯一杯地灌下去,我没有劝阻,只是不停地翻转手中的酒杯,只有在这个时候才会有一丝的成就感吧,只要松手酒杯就会粉碎,原来掌握别人的命运的感觉是那样的好。也许我们只是上帝手中的酒杯吧,自己一定是被喝醉了的上帝失手跌落破碎一地。抬头看着有脸醉意的柔若,是否我们的命运,就象那破碎的酒杯与酒?注定了的悲剧? 酒杯破碎了一地,散发着酒香的液体毫不留恋的离开那已经破碎的酒杯,玻璃酒杯上,挂着几滴透明的液体,那是尚未流出的酒?还是酒杯的泪滴?一定是酒杯的眼泪吧,眼睁睁的看者一直占倨在自己内心的东西离开自己却没有能力留住而落泪吧? 她终于没有了再喝下去的意思,我说,柔若,我送你回家吧! 家?手指抚过冰冷的酒瓶,凄凉地笑道,家,多么陌生的字眼啊,你只要送我出去亦足够。 门外,停着一辆小轿车,她爬上车,对着车外的我挥手言别,透过挡风玻璃,我看到一张令人厌恶的嘴脸,贪婪的眼神游走在一脸醉意,满心无所谓的柔若身上。 柔若的事,我是非常清楚的,只是我没有能力劝阻,她说,驰,你知道吗?我要作贱自己让铭愧疚,我要让他为对我的伤害付出代价。疲惫的脸色上,写满了幼稚的固执。 她说,驰,我知道铭是在乎我的,因为他打了好几次电话骂我,那声音,令我难忘,至少让我知道,铭是在乎我的;她说,驰,你知道吗?我也曾想过彻底地忘掉关于铭的回忆,可人生不是一台记忆机器,不是删除就能消失,当我不停地换着一个又一个男友的时候,我脑海中浮现的依旧只是铭的笑脸。 有时候,我只有默默地倾听,任痛苦在心底无限蔓延。为什么你要那样作贱你自己呢?难道就不可以好好过常人的生活吗?想起对她大声训斥的那次,她畏缩在墙角流泪的样子让我没了往下说的勇气。 她说,你不会明白的,我从10岁以后就开始堕落,认识铭之后只是将堕落加深了一点。 她说,我们不是同一个世界的人,你无法明白我的生存方式。现实生活中总是存在太多的被逼无奈,只是你无法明白。 我笑笑,有些事,是注定无法明白的吧,比如我们的相遇。 我开始催促她快点动身去她妈妈那,我只想她结束流浪堕落的生活,等我毕业。 而她,总是在这个时候用复杂的眼神看着我的眼睛,我以为我读懂了她的眼神可实际上我什么也不懂。 窗外是倾盆的大雨,我想冲出去冲洗自己沉重的灵魂,却在起身的刹那听到心碎的声音。我们都是这个世界受伤最重的孩子,再圣洁的雨水也无法洗涤我们带血的伤痕。{dotted startColor="#ff6c6c" endColor="#1989fa"/}{mtitle title="6"/} 依旧记得分别那天,天空异常的晴朗,我们在车站的一角享受阳光刺肤的灼伤,我只想用皮肤的伤痛让自己忘掉内心的离伤。 她抬头用手不停地给自己扇风,纤弱的细指在阳光下异常好看,她说,驰,为什么不留我呢?只要你留我,我就留下。 我说,我只想你好好地呆在你妈身边,忘掉不愉快的过去,等我毕业。我不在乎你的过去,我只想结束你堕落的生活,给你一个安定的人生。 不可能的,她说,我已经不可能回头了,还是忘了我吧!她总是用这样的话避开我的承诺。她说,人与人之间感情太复杂不好,你是聪明人,应该明白,做知己好。男女关系只是一时的,而知己却是一世的。 可是,我只想保护你一辈子,不想你过那种堕落的生活,是你真的不能回头还是你不愿回头?柔若,要怎样你才明白,只要你回头,我一直站在你身后。 可是,这一切你都不会知道,因为我明白,你是不会回头的,有些事,明知不可以做却做了,是命运,她说,有些时光,不应该错过却错过了是无奈。奈何我已错过你的童年,你已经成了一个有故事的人,时光将我们之间拉开了一条长长的河,那是我永远无法逾越的宽度——奈若何? 当我知道她发生的事后,我开始后悔当时的冲动,或许我留下柔若,她也许不会堕落成那样,那是我第一次对着话筒潸然泪下。我说我不知道你妈是那样的人,害了自己连自己的女儿也要害,都是我的错,我不该叫你去你妈身边,我以为是帮你,可我做到的反是把你从一个火坑推向另一个更大的火坑。 柔若依旧是孩子般的语气说着成熟老练的字句,她说,我们这里的野男人都很大方,随便一夜都有万八千的,她用平静的语气叙述,仿若局外人。 可是柔若,你知道我的感受吗? 我没有说话,我开始明白为什么那天的分别天气是那样的好,别人的离别是雨水绵绵,忧伤的格调换来的是下一次美好的相聚,而美好的分别却是悲剧的开始。 第二年春传来柔若自杀的消息,我已没有流泪。我是知道的,这个世界有许多不愿发生的事总是会毫不留情地发生,我们没有能力改变只有眼睁睁地看着悲剧发生。 铭依旧会不时提起柔若,我知道他是真心喜欢她的,或许是为我才狠心伤她吧!只是我亦不再问,他亦不再提。{mtitle title="完"/}
2021年03月04日
98 阅读
1 评论
0 点赞
2021-03-04
使用Jenkins一键打包部署SpringBoot应用
Jenkins简介Jenkins是开源CI&CD软件领导者,提供超过1000个插件来支持构建、部署、自动化,满足任何项目的需要。我们可以用Jenkins来构建和部署我们的项目,比如说从我们的代码仓库获取代码,然后将我们的代码打包成可执行的文件,之后通过远程的ssh工具执行脚本来运行我们的项目。一:Jenkins的安装1.下载Jenkins的Docker镜像 docker pull jenkins/jenkins:lts2.在Docker容器中运行Jenkinsdocker run -p 8080:8080 -p 50000:5000 --name jenkins \ -u root \ -v /mydata/jenkins_home:/var/jenkins_home \ -d jenkins/jenkins:lts3.Jenkins的配置①、运行成功后访问该地址登录Jenkins,第一次登录需要输入管理员密码:http://IP地址:8080/,通过docker logs jenkins 查看启动日志获取initialAdminPassword密码docker logs jenkins确保以下插件被正确安装1.根据角色管理权限的插件:Role-based Authorization Strategy2.远程使用ssh的插件:SSH②、通过系统管理->全局工具配置来进行全局工具的配置,比如maven的配置新增maven的安装配置:③、在系统管理->系统配置中添加全局ssh的配置,这样Jenkins使用ssh就可以执行远程的linux脚本了:④、角色权限管理我们可以使用Jenkins的角色管理插件来管理Jenkins的用户,比如我们可以给管理员赋予所有权限,运维人员赋予执行任务的相关权限,其他人员只赋予查看权限。在系统管理->全局安全配置中启用基于角色的权限管理:⑤、进入系统管理->Manage and Assign Roles界面:⑥、添加角色与权限的关系:二、Docker安装Gitlab (https://www.cnblogs.com/diaomina/p/12830449.html)# 1、查找GitLab镜像 docker search gitlab # 2、拉取gitlab docker镜像 docker pull gitlab/gitlab-ce:latest # 3、运行GitLab并运行容器 docker run \ -itd \ -p 9980:80 \ -p 9922:22 \ -v /usr/local/gitlab/etc:/etc/gitlab \ -v /usr/local/gitlab/log:/var/log/gitlab \ -v /usr/local/gitlab/opt:/var/opt/gitlab \ --restart always \ --privileged=true \ --name gitlab \ gitlab/gitlab-ce # 4进入容器内 docker exec -it gitlab /bin/bash # 5、修改gitlab.rb (先查看下一个步骤再决定是否进行本步骤,本步骤是可以跳过的) # 打开文件 vi /etc/gitlab/gitlab.rb # 这个文件是全注释掉了的,所以直接在首行添加如下配置 # gitlab访问地址,可以写域名。如果端口不写的话默认为80端口 (图片上拼写错误,正确的是external_url) external_url 'http://172.16.107.194:9980' # ssh主机ip gitlab_rails['gitlab_ssh_host'] = '172.16.107.194' # ssh连接端口 gitlab_rails['gitlab_shell_ssh_port'] = 9922 # 6、修改gitlab.yml (这一步原本不是必须的,因为gitlab.rb内配置会覆盖这个,为了防止没有成功覆盖所以我在这里进行配置,当然你也可以选择不修改gitlab.rb直接修改这里) # 打开文件 vi /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml # 配置一:找到gitlab标签,将其子标签如下修改 hotst:172.16.107.194 port: 9980 https: false ssh_host:172.16.107.194 # 配置二:找到gitlab_shell标签下的ssh_port,将其修改为9922 # 7、让修改后的配置生效 gitlab-ctl reconfigure # 8、重启gitlab gitlab-ctl restart命令解释:-i 以交互模式运行容器,通常与 -t 同时使用命令解释:-t 为容器重新分配一个伪输入终端,通常与 -i 同时使用-d 后台运行容器,并返回容器ID-p 9980:80 将容器内80端口映射至宿主机9980端口,这是访问gitlab的端口-p 9922:22 将容器内22端口映射至宿主机9922端口,这是访问ssh的端口-v /usr/local/gitlab-test/etc:/etc/gitlab 将容器/etc/gitlab目录挂载到宿主机/usr/local/gitlab-test/etc目录下,若宿主机内此目录不存在将会自动创建,其他两个挂载同这个一样--restart always 容器自启动--privileged=true 让容器获取宿主机root权限--name gitlab-test 设置容器名称为gitlab-testgitlab/gitlab-ce 镜像的名称,这里也可以写镜像ID将代码上传到Git仓库执行脚本准备
2021年03月04日
110 阅读
0 评论
0 点赞
2021-03-04
SpringBoot整合RabbitMQ实现延迟消息
整合RabbitMQ实现延迟消息的过程,以发送延迟消息取消超时订单为例。1.Docker下安装RabbitMQsudo docker pull rabbitmq:management docker run -dit --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:management docker update rabbitmq--restart=always2.RabbitMQ下增加用户及角色 3.rabbitmq的消息模型标志中文名英文名描述P生产者Producer消息的发送者,可以将消息发送到交换机C消费者Consumer消息的接收者,从队列中获取消息进行消费X交换机Exchange接收生产者发送的消息,并根据路由键发送给指定队列Q队列Queue存储从交换机发来的消息type交换机类型typedirect表示直接根据路由键(orange/black)发送消息4.SpringBoot项目集成RabbitMQ修改pom.xm文件<!--消息队列相关依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>修改application.ymlrabbitmq: port: 5672 host: 172.16.107.192 virtual-host: shop username: shop password: shop4.1具体业务代码添加消息队列的枚举配置类QueueEnumpackage com.bdego.modules.shop.enums; import lombok.Getter; /** * 商城订单模块消息队列枚举配置 */ @Getter public enum OrderQueueEnum { /** * 消息通知队列 */ QUEUE_ORDER_CANCEL("shop.order.direct", "shop.order.cancel", "shop.order.cancel"), /** * 消息通知ttl队列 */ QUEUE_TTL_ORDER_CANCEL("shop.order.direct.ttl", "shop.order.cancel.ttl", "shop.order.cancel.ttl"); /** * 交换机名称 */ private String exchange; /** * 队列名称 */ private String name; /** * 路由键 */ private String routeKey; OrderQueueEnum(String exchange, String name, String routeKey) { this.exchange = exchange; this.name = name; this.routeKey = routeKey; } }添加RabbitMQ的配置,用于配置交换机、队列及队列与交换机的绑定关系。package com.bdego.config; import com.bdego.modules.shop.enums.OrderQueueEnum; import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 消息队列配置 */ @Configuration public class RabbitMqConfig { /** * 订单消息实际消费队列所绑定的交换机 */ @Bean DirectExchange orderDirect() { return (DirectExchange) ExchangeBuilder .directExchange(OrderQueueEnum.QUEUE_ORDER_CANCEL.getExchange()) .durable(true) .build(); } /** * 订单延迟队列所绑定的交换机 */ @Bean DirectExchange orderTtlDirect() { return (DirectExchange) ExchangeBuilder .directExchange(OrderQueueEnum.QUEUE_TTL_ORDER_CANCEL.getExchange()) .durable(true) .build(); } /** * 订单实际消费队列 */ @Bean public Queue orderQueue() { return new Queue(OrderQueueEnum.QUEUE_ORDER_CANCEL.getName()); } /** * 订单延迟队列(死信队列) */ @Bean public Queue orderTtlQueue() { return QueueBuilder .durable(OrderQueueEnum.QUEUE_TTL_ORDER_CANCEL.getName()) .withArgument("x-dead-letter-exchange", OrderQueueEnum.QUEUE_ORDER_CANCEL.getExchange())//到期后转发的交换机 .withArgument("x-dead-letter-routing-key", OrderQueueEnum.QUEUE_ORDER_CANCEL.getRouteKey())//到期后转发的路由键 .build(); } /** * 将订单队列绑定到交换机 */ @Bean Binding orderBinding(DirectExchange orderDirect,Queue orderQueue){ return BindingBuilder .bind(orderQueue) .to(orderDirect) .with(OrderQueueEnum.QUEUE_ORDER_CANCEL.getRouteKey()); } /** * 将订单延迟队列绑定到交换机 */ @Bean Binding orderTtlBinding(DirectExchange orderTtlDirect,Queue orderTtlQueue){ return BindingBuilder .bind(orderTtlQueue) .to(orderTtlDirect) .with(OrderQueueEnum.QUEUE_TTL_ORDER_CANCEL.getRouteKey()); } }交换机及队列说明shop.order.direct(取消订单消息队列所绑定的交换机):绑定的队列为shop.order.cancel,一旦有消息以shop.order.cancel为路由键发过来,会发送到此队列。shop.order.direct.ttl(订单延迟消息队列所绑定的交换机):绑定的队列为shop.order.cancel.ttl,一旦有消息以shop.order.cancel.ttl为路由键发送过来,会转发到此队列,并在此队列保存一定时间,等到超时后会自动将消息发送到shop.order.cancel(取消订单消息消费队列)。添加延迟消息的发送者CancelOrderSenderpackage com.bdego.modules.shop.mq.component; import com.bdego.modules.shop.mq.enums.OrderQueueEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.stereotype.Component; import javax.annotation.Resource; /** * 取消订单消息的发出者 */ @Slf4j @Component public class CancelOrderSender { @Resource private AmqpTemplate amqpTemplate; public void sendMessage(Long orderId,final long delayTimes){ //给延迟队列发送消息 amqpTemplate.convertAndSend(OrderQueueEnum.QUEUE_TTL_ORDER_CANCEL.getExchange(), OrderQueueEnum.QUEUE_TTL_ORDER_CANCEL.getRouteKey(), orderId, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { //给消息设置延迟毫秒值 message.getMessageProperties().setExpiration(String.valueOf(delayTimes)); return message; } }); log.info("send delay message orderId:{}",orderId); } }添加取消订单消息的接收者CancelOrderReceiverpackage com.bdego.modules.shop.mq.component; import com.bdego.modules.shop.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * 取消订单消息的处理者 */ @Slf4j @Component @RabbitListener(queues = "shop.order.cancel") public class CancelOrderReceiver { @Autowired private OrderService orderService; @RabbitHandler public void handle(Long orderId){ log.info("receive delay message orderId:{}",orderId); orderService.cancelOrder(orderId); } }
2021年03月04日
110 阅读
0 评论
0 点赞
2021-03-04
Centos 7 下 Docker安装及常用指令
1.删除服务器中原本安装的dockeryum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine2.安装yum-utilsyum install -y yum-utils device-mapper-persistent-data lvm23.为yum源添加docker仓库位置yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo4.yum安装dockeryum install -y docker-ce docker-ce-cli containerd.io5.设置docker开机自启systemctl start docker systemctl enable docker6.常用命令# 1.查看容器状态 docker ps docker ps -a # 2.进入容器 docker exec -it mysql(名字或者ID) /bin/bash # 3.镜像自启动 docker update redis --restart=always docker update mysql --restart=always # 4.搜索镜像 docker search java # 5.下载镜像 docker pull java:8 # 6.列出镜像 docker images # 7.删除镜像 - 指定名称删除镜像:docker rmi java:8 - 指定名称删除镜像(强制):docker rmi -f java:8 - 删除所有没有引用的镜像:docker rmi `docker images | grep none | awk '{print $3}'` - 强制删除所有镜像:docker rmi -f $(docker images) # 8.打包镜像 - -t 表示指定镜像仓库名称/镜像名称:镜像标签 .表示使用当前目录下的Dockerfile文件 docker build -t mall/mall-admin:1.0-SNAPSHOT . # 9.推送镜像 - 登录Docker Hub docker login - 给本地镜像打标签为远程仓库名称 docker tag mall/mall-admin:1.0-SNAPSHOT macrodocker/mall-admin:1.0-SNAPSHOT - 推送到远程仓库 docker push macrodocker/mall-admin:1.0-SNAPSHOT # 10.停止容器 docker stop nginx # 11.启动容器 docker start nginx # 12.删除指定容器 docker rm nginx # 13.查看容器全部日志 docker logs nginx # 14.动态查看容器日志 docker logs -f nginx # 15.查看容器的IP地址 docker inspect --format '{{ .NetworkSettings.IPAddress }}' nginx # 16.修改容器的启动方式,将容器启动方式改为always docker container update --restart=always nginx # 17.同步宿主机时间到容器 docker cp /etc/localtime nginx:/etc/ # 18.指定容器时区 docker run -p 80:80 --name nginx \ -e TZ="Asia/Shanghai" \ -d nginx:1.17.0 # 19.查看容器资源占用状况, - 查看指定容器资源占用状况,比如cpu、内存、网络、io状态:docker stats nginx - 查看所有容器资源占用情况:docker stats -a - 查看所有容器 docker stats CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS # 20.查看容器磁盘使用情况 docker system df # 21.使用root账号进入容器内部 docker exec -it --user root $ContainerName /bin/bash # 22.查看所有网络 docker network ls # 23.创建外部网络 docker network create -d bridge my-bridge-network 查看网络 docker network inspect my-bridge-network # 24.指定容器网络 docker run -p 80:80 --name nginx \ --network my-bridge-network \ -d nginx:1.17.0 # 25.查看Docker镜像的存放位置: docker info | grep "Docker Root Dir" # 26.查看Docker占用的磁盘空间情况: docker system df # 27.删除所有关闭的容器: docker ps -a | grep Exit | cut -d ' ' -f 1 | xargs docker rm # 28.删除所有dangling镜像(没有Tag的镜像): docker rmi $(docker images | grep "^<none>" | awk "{print $3}") # 29.删除所有dangling数据卷(即无用的 volume): docker volume rm $(docker volume ls -qf dangling=true) # 30.停用并删除所有运行中容器 docker stop $(docker ps -q) & docker rm $(docker ps -aq) # 31.删除那些已停止的容器、dangling 镜像、未被容器引用的 network 和构建过程中的 cache docker system prune # 32.按照条件删除镜像 docker rmi --force `docker images | grep 条件值 | awk '{print $3}'` 7.配置镜像加速器针对Docker客户端版本大于 1.10.0 的用户您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://n6gjvesl.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker8.Docker创建镜像一些指令指令举例:docker run \ --detach \ -p 8443:443 \ -p 8090:80 \ -p 9922:22 \ -v /mydata/gitlab/etc:/etc/gitlab \ -v /mydata/gitlab/log:/var/log/gitlab \ -v /mydata/gitlab/data:/var/opt/gitlab \ --privileged=true \ --name gitlab \ gitlab/gitlab-ce-v:将宿主机上的文件挂载到宿主机上,格式为:宿主机文件目录:容器文件目录;-i: 以交互模式运行容器,通常与 -t 同时使用命令解释:-t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用-d: 后台运行容器,并返回容器ID-e: 指定参数,比如给myql设置密码-p: 8090:80 将容器内80端口映射至宿主机9980端口,这是访问gitlab的端口-p: 9922:22 将容器内22端口映射至宿主机9922端口,这是访问ssh的端口--restart always 容器自启动--privileged=true 让容器获取宿主机root权限--name gitlab设置容器名称为gitlabgitlab/gitlab-ce 镜像的名称,这里也可以写镜像ID9.安装补全工具yum install -y bash-completion一、Docker ComposeCompose 简介Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。Compose 使用的三个步骤:使用 Dockerfile 定义应用程序的环境。使用 docker-compose.yml 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。最后,执行 docker-compose up 命令来启动并运行整个应用程序。docker-compose.yml 的配置案例如下(配置参数参考下文):实例:# yaml 配置实例 version: '3' services: web: build: . ports: - "5000:5000" volumes: - .:/code - logvolume01:/var/log links: - redis redis: image: redis volumes: logvolume01: {}Compose 安装LinuxLinux 上我们可以从 Github 上下载它的二进制包来使用,最新发行的版本地址:https://github.com/docker/compose/releases。运行以下命令以下载 Docker Compose 的当前稳定版本:$ sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose{message type="info"}要安装其他版本的 Compose,请替换 1.24.1。{/message}将可执行权限应用于二进制文件:$ sudo chmod +x /usr/local/bin/docker-compose创建软链:$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose测试是否安装成功:$ docker-compose --version{message type="info"}注意: 对于 alpine,需要以下依赖包: py-pip,python-dev,libffi-dev,openssl-dev,gcc,libc-dev,和 make。{/message}二、Docker Compose使用2.1.准备工作2.1.1 创建一个测试目录:$ mkdir composetest $ cd composetest2.1.2 在测试目录中创建一个名为 app.py 的文件,并复制粘贴以下内容:import time import redis from flask import Flask app = Flask(__name__) cache = redis.Redis(host='redis', port=6379) def get_hit_count(): retries = 5 while True: try: return cache.incr('hits') except redis.exceptions.ConnectionError as exc: if retries == 0: raise exc retries -= 1 time.sleep(0.5) @app.route('/') def hello(): count = get_hit_count() return 'Hello World! I have been seen {} times.\n'.format(count){message type="info"}在此示例中,redis 是应用程序网络上的 redis 容器的主机名,该主机使用的端口为 6379。{/message}2.1.3在 composetest 目录中创建另一个名为 requirements.txt 的文件,内容如下:flask redis2.2 创建 Dockerfile 文件2.2.1 在 composetest 目录中,创建一个名为的文件 Dockerfile,内容如下:FROM python:3.7-alpine WORKDIR /code ENV FLASK_APP app.py ENV FLASK_RUN_HOST 0.0.0.0 RUN apk add --no-cache gcc musl-dev linux-headers COPY requirements.txt requirements.txt RUN pip install -r requirements.txt COPY . . CMD ["flask", "run"]2.2.2 Dockerfile 内容解释:①:FROM python:3.7-alpine: 从 Python 3.7 映像开始构建镜像。②:WORKDIR /code: 将工作目录设置为 /code。③:ENV FLASK_APP app.py ENV FLASK_RUN_HOST 0.0.0.0 设置 flask 命令使用的环境变量。④:RUN apk add --no-cache gcc musl-dev linux-headers: 安装 gcc,以便诸如 MarkupSafe 和 SQLAlchemy 之类的 Python 包可以编译加速。⑤:COPY requirements.txt requirements.txt RUN pip install -r requirements.txt 复制 requirements.txt 并安装 Python 依赖项。⑥:COPY . .: 将 . 项目中的当前目录复制到 . 镜像中的工作目录。⑦:CMD ["flask", "run"]: 容器提供默认的执行命令为:flask run。2.3 创建 docker-compose.yml2.3.1 在测试目录中创建一个名为 docker-compose.yml 的文件,然后粘贴以下内容:# yaml 配置 version: '3' services: web: build: . ports: - "5000:5000" redis: image: "redis:alpine"该 Compose 文件定义了两个服务:web 和 redis。①:web:该 web 服务使用从 Dockerfile 当前目录中构建的镜像。然后,它将容器和主机绑定到暴露的端口 5000。此示例服务使用 Flask Web 服务器的默认端口 5000 。②:redis:该 redis 服务使用 Docker Hub 的公共 Redis 映像。2.4 使用 Compose 命令构建和运行您的应用2.4.1 在测试目录中,执行以下命令来启动应用程序:docker-compose up2.4.2 如果你想在后台执行该服务可以加上 -d 参数:docker-compose up -d
2021年03月04日
172 阅读
0 评论
0 点赞
1
2