一直有将前台改写成vue的打算,最近刚把项目样子搭出来,同学们可以来看下支持下点点star,未来计划去整合~
仓库地址
在线演示
架构图及微服务划分
环境配置
Linux 环境
JDK
yum install java-1.8.0-openjdk.x86_64
java -version
vi /etc/profile
#set java environment
JAVA_HOME=/usr/lib/jvm/jre-1.8.0-openjdk
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JAVA_HOME CLASSPATH PATH
Docker
- 卸载系统之前的docker
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
- 设置存储库
sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
- 安装DOCKER引擎
sudo yum install docker-ce docker-ce-cli containerd.io
- 启动Docker.
sudo systemctl start docker
- 配置镜像加速
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://ozz4irqv.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
- 开机自启
sudo docker update redis --restart=always
MySQL (Docker)
- 拉取 mysql镜像
sudo docker pull mysql:8.0
- 启动mysql容器
# --name指定容器名字 -v目录挂载(左主右从) -p指定端口映射 -e设置mysql参数 -d后台运行
sudo docker run --name mysql -v /usr/local/mysql/data:/var/lib/mysql -v /usr/local/mysql:/etc/mysql/conf.d -v /usr/local/mysql/log:/var/log/mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:8.0
- 进入mysql容器
docker exec -it a4435a23e7a470297fded7fbdeb1a06045530e1631517585f490c680e4039891 bin/bash
- 开机自启
sudo docker update mysql --restart=always
Redis(Docker)
- 拉取redis镜像到本地
docker pull redis
- 修改需要自定义的配置(docker-redis默认没有配置文件,自己在宿主机建立后挂载映射)
位置在 /usr/local/redis/redis.conf
#开启远程权限
bind 0.0.0.0
#开启aof持久化
appendonly yes
- 启动redis服务运行容器
docker run --name redis -v /usr/local/redis/data:/data -v /usr/local/redis/redis.conf:/usr/local/etc/redis/redis.conf -p 6379:6379 -d redis redis-server /usr/local/etc/redis/redis.conf
- 连接 redis
docker exec -it redis redis-cli
Nginx(Docker)
# 终极版!
docker run -p 80:80 --name nginx \
-v /usr/local/nginx/html:/usr/share/nginx/html \
-v /usr/local/nginx/conf/nginx.conf/:/etc/nginx/nginx.conf \
-v /usr/local/nginx/logs:/var/log/nginx \
-d nginx
docker run -p 80:80 --name nginx \
-v /Users/june/AServerMiddleware/nginx-docker/html:/usr/share/nginx/html \
-v /Users/june/AServerMiddleware/nginx-docker/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /Users/june/AServerMiddleware/nginx-docker/conf/default.conf:/etc/nginx/conf.d/default.conf \
-v /Users/june/AServerMiddleware/nginx-docker/logs:/var/log/nginx -d nginx
RabbitMQ(Docker)
Docker系列之RabbitMQ安装部署教程 - 云+社区 - 腾讯云 (tencent.com)
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=june -e RABBITMQ_DEFAULT_PASS=L200107208017./@ rabbitmq:management
# 4369 25672 Erlang发现&集群端口
# 5671 5672 AMQP 端口
# 15672 web管理后台端口
# 61613 61614 STOMP协议端口
# 1883 8883 MQTT 协议端口
Nacos(Docker) Linux 部署 Mac 不支持
https://www.jianshu.com/p/3d3e17bc629f
# 1.创建本地配置文件
mkdir -p /home/nacos/logs/ #新建logs目录
mkdir -p /home/nacos/init.d/
vim /home/nacos/init.d/custom.properties #修改配置文件
# 2.添加如下内容
server.contextPath=/nacos
server.servlet.contextPath=/nacos
server.port=8848
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://sh-cdb-0ej7ogfe.sql.tencentcdb.com:58887/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=L200107208017@
nacos.cmdb.dumpTaskInterval=3600
nacos.cmdb.eventTaskInterval=10
nacos.cmdb.labelTaskInterval=300
nacos.cmdb.loadDataAtStart=false
management.metrics.export.elastic.enabled=false
management.metrics.export.influx.enabled=false
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{
User-Agent}i
nacos.security.ignore.urls=/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/login,/v1/console/health/**,/v1/cs/**,/v1/ns/**,/v1/cmdb/**,/actuator/**,/v1/console/server/**
nacos.naming.distro.taskDispatchThreadCount=1
nacos.naming.distro.taskDispatchPeriod=200
nacos.naming.distro.batchSyncKeyCount=1000
nacos.naming.distro.initDataRatio=0.9
nacos.naming.distro.syncRetryDelay=5000
nacos.naming.data.warmup=true
nacos.naming.expireInstance=true
# 3.启动容器
docker run \
--name nacos -d \
-p 8848:8848 \
--privileged=true \
--restart=always \
-e JVM_XMS=256m \
-e JVM_XMX=256m \
-e MODE=standalone \
-e PREFER_HOST_MODE=hostname \
-v /home/nacos/logs:/home/nacos/logs \
-v /home/nacos/init.d/custom.properties:/home/nacos/init.d/custom.properties \
nacos/nacos-server:1.4.2
Sentinel
Linux直接部署
# 注意,不要同时在开发机器部署微服务,云服务器部署sentinel,因为sentinel也是需要访问本机的!
java -Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard -Dsentinel.dashboard.auth.username=sentinel-qs -Dsentinel.dashboard.auth.password=L200107208017@ -jar sentinel-dashboard-1.8.1.jar
本机部署
nohup java -server -Xms64m -Xmx256m -Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard -jar /Users/june/AServerMiddleware/sentinel/sentinel-dashboard-1.8.1.jar
Zipkin(Docker)
zipkin 使用外部 MySQL 持久化存储 - 简书 (jianshu.com)
docker run -d -p 9411:9411 openzipkin/zipkin:latest
-e JAVA_OPTS=-Xmx128m \
docker run \
--name zipkin-server -d \
--restart=always \
-p 9411:9411 \
-e MYSQL_USER=root \
-e MYSQL_PASS=L200107208017@ \
-e MYSQL_HOST=sh-cdb-0ej7ogfe.sql.tencentcdb.com \
-e STORAGE_TYPE=mysql \
-e MYSQL_DB=zipkin \
-e MYSQL_TCP_PORT=58887 \
openzipkin/zipkin
开发机器环境
git
git config --global user.name "June"
git config --global user.email "[email protected]"
ssh-keygen -t rsa -C "[email protected]"
# 查看生成的密钥内容
cat ~/.ssh/id_rsa.pub
# 复制密钥内容到 gitee,以后该机器就推送内容不用输入密码
# 测试
ssh -T [email protected]
# gitignore 中添加以下内容
**/mvnw
**/mvnw.cmd
**/.mvn
**/target
.idea
**/.gitignore
**/README.md
Node.js
- 官网下载 node.js 附带有 npm
- 配置 npm 镜像
npm config set registry http://registry.npm.taobao.org/
项目构架
renren-generator
利用这个模块给每一个业务模块生成代码,以 product 模块为例
选用技术
SpringCloud Alibaba - Nacos (服务发现/注册)
SpringCloud Alibaba - Nacos (动态配置管理)
SpringCloud Alibaba - Seata (分布式事务解决方案)
SpringCloud Alibaba - Sentinel (限流、降级、熔断等)
SpringCloud - Ribbon (负载均衡)
SpringCloud - Feign 远程调用服务
SpringCloud - Gateway (API网关)
SpringCloud - Sleuth (调用链监控)
核心依赖
这块配不好,踩坑少不了
<!--聚合服务-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--子服务-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/>
</parent>
<!--子服务-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
技术要点
Nacos 注册中心
- 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 指定配置中心位置
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
- 添加注解
@EnableDiscoveryClient
OpenFeign 远程调用
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 指定配置中心位置
- 创建接口,指定方法
- 添加注解,上图中一个,启动类一个
@EnableFeignClients(basePackages = "org.june.member.feign")
Nacos 配置中心
- 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-config</artifactId>
</dependency>
- 创建
bootstrap.properties
指定配置中心位置以及自己的服务名称
spring.application.name=mall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
- Nacos 配置中心添加
服务名.properties
配置文件,这个文件可以动态读取 - 需要使用 动态配置的代码类头添加
@RefreshScope
,使用@Value("${name.age}")
(在成员变量处)方式获取
注:
- 配置中心配置项优先级较本地文件优先级高
- 经测试,第三步中 yaml yml 都无法识别,只有properties可以识别
命名空间的说明
# 追加在 bootstrap.properties 中
spring.cloud.nacos.config.namespace=3ce35e9e-4e10-44df-b1a4-8fd753c3e4ea
配置中心的说明
# 追加在 bootstrap.properties 中
spring.cloud.nacos.config.group=ddd
类似于上面的命名空间,都是用来隔离文件的
最终实践
spring.application.name=mall-coupon
# 服务注册/发现
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 配置中心 这两项可以只配 第二个
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=2863031e-ae6a-4bff-903e-48d27201e1ab
spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[0].group=dev
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=true
spring.cloud.nacos.config.ext-config[2].data-id=others.yml
spring.cloud.nacos.config.ext-config[2].group=dev
spring.cloud.nacos.config.ext-config[2].refresh=true
Gateway 网关
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 指定配置中心(配置文件、启动类注解)
- 配置文件写路由
注:SpringBoot 2.3.1.RELEASE
对应 SpringCloud Hoxton.SR6
不会报错
Spring Cloud Gateway 2.1.0 中文官网文档 - 云+社区 - 腾讯云 (tencent.com)
ES6
ECMAScript 是浏览器脚本语言的规范,JavaScript 是该规范的具体实现。以下示例以 JavaScript 为例
变量
- var 变量声明变量往往会越狱,只能声明一次
- let 声明变量有严格作用域,可以声明多次
- const 声明变量不允许改变
解构表达式
let arr = [1,2,3]
// ↓
let [a,b,c] = arr
/
const person = {
name: "June",
age: 21
}
// ↓
const {
name:var1,age:var2} = person;
console.log(var1,var2)
字符串扩展
let str = "helloworld"
str.startsWith()
str.endsWith()
str.includes()
模板字符串
let var1 = "June"
let var2 = "March"
let ss = `${
var1}
this is a test
${
var2}`
console.log(ss)
函数参数
function test1(a,b){
...
}
test(var1) // 只传一个
///
function test2(...vars){
...
}
test(var1,var2) // 传多个
箭头函数
var print = function(obj){
console.log(obj)
}
// ↓
var print = obj => console.log(obj)
/ 对象解构
var person = {
name:"jack",
age:21
}
var hello = ({
name}) => console.log("hello," + name)
对象优化
var person = {
name:"jack",
age:21
}
Object.keys(person) -> ["name","age"]
Object.values(person) -> ["jack",21]
Object.entries(person) -> [Array(2),Array(2),Array(2)]
// 追加
const target = {
a:1}
const source1 = {
b:2}
const source2 = {
c:3}
Object.assign(target, source1, source2)
target -> {
a:1, b:2, c:3}
// 对象简写1
const age = 21
const name = "张三"
const person = {
age,name}
// 对象简写2
let person = {
name: "jack",
eat: function(food){
console.log(this.name + "在吃" + food)
},
eat2: food => console.log(person.name + "在吃" + food)
}
/// 拷贝对象(深拷贝)
let p1 = {
name: "Any", age:15}
let p2 = {
...p1}
/// 对象合并(会覆盖)
let age = {
age:15}
let name = {
name: "Amy"}
let person = {
...age,...name}
数组增强
let arr = ['1', '2', '3', '4']
arr.map(item)=>{
return item*2
} // => [2,4,6,8]
// ↓
arr = arr.map(item=>item*2)
let result = arr.reduce((a,b)=>{
return a+b
},100) // => 110
模块化
/ js1.js
var name = "jack"
var age = 21
function add(a,b){
return a + b
}
export{
name,age,add}
// js2.js
import {
name,age,add} from "./js1.js"
add(1,2)
Vue
v-text v-html
文本值绑定
<div id="app">
<h1>{
{ name }}</h1>
<h1 v-text="name1"></h1>
<span v-html="name2"></span>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
name: '张三d',
name1: '张三2',
name2: '<h1>张三3</h1>'
}
})
</script>
v-bind
属性值绑定,一般用于 href、class、style
<a v-bind:href:"link">gogogo</a>
<div id="app">
<span v-bind:class="{active:isActive,'text-danger':hasError}"
v-bind:style="{color:color1,frontSize:size}">你好</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
isActive:true,
hasError:true,
color1:'red',
size:'36px'
}
})
</script>
v-model
双向绑定,不同于以上两者
v-on
事件绑定
v-for
遍历
v-if v-show
v-if 条件为 true,元素才会被渲染;v-show 条件为true,元素才会显示
前者是注释掉了相关代码,后者把样式改变了
计算属性、侦听器和过滤器
<h1>
{
{totalPrice}}
</h1>
<script>
new Vue({
...
data:{
a: 5,
b: 6
},
<!--计算属性-->
computed:{
totalPrice(){
return a+b
}
},
<!--侦听器-->
watch:{
a: function(newVal,oldVal){
...
}
}
<!--过滤器-->
filters:{
genderFilter(val){
if(val ==1){
return '男'
} else{
return '女'
}
}
<!--
调用
{
{user.gender | genderFilter}}
-->
}
})
</script>
<script>
<!--全局过滤器-->
Vue.filter("gFilter",function(val)){
if(val ==1){
return '男'
} else{
return '女'
}
}
</script>
组件化
生命周期和钩子函数
vue 模块化开发
sudo npm install webpack -g
npm install -g @vue/cli-init
sudo npm install --global vue-cli
# 再创建项目文件夹
vue init webpack vue-demo
cd vue-demo
npm run dev
Elasticsearch
基本概念
- Index(索引)
MySQL的库
- Type(类型)
在Index中,可以定义一个或多个类型,类似于MySQL中的表;每一种类型的数据放在一起;
- Document(文档)
保存在某个索引下,某种类型的一个数据,文档是JSON格式的,Document就像是MySQL中某个Table里面的内容
- 倒排索引
创建实例
- elasticsearch
# Docker !8版本需要额外配置东西
docker pull elasticsearch:7.17.0
docker pull kibana:7.17.0
# 创建
mkdir -p /Users/june/AServerMiddleware/elsatic-docker/plugins
mkdir -p /Users/june/AServerMiddleware/elsatic-docker/config
mkdir -p /Users/june/AServerMiddleware/elsatic-docker/data
#
echo "http.host: 0.0.0.0" >/Users/june/AServerMiddleware/elsatic-docker/config/elasticsearch.yml
#
chmod -R 777 /Users/june/AServerMiddleware/elsatic-docker
# 启动Elastic search
# 9200是用户交互端口 9300是集群心跳端口
# -e指定是单阶段运行
# -e指定占用的内存大小,生产时可以设置32G
sudo docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /Users/june/AServerMiddleware/elsatic-docker/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /Users/june/AServerMiddleware/elsatic-docker/data:/usr/share/elasticsearch/data \
-v /Users/june/AServerMiddleware/elsatic-docker/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.17.0
- kibana
sudo docker run --name kibana -e ELASTICSEARCH_HOSTS=http://124.222.22.217:9200 -p 5601:5601 -d kibana:7.17.0
# 配置中文
docker cp 源 目的 # 容器写法 容器ID:路径 主机直接写路径
docker exec -it ID /bin/bash
kibana.yml 中添加 i18n.locale: "zh-CN"
初步检索
1._cat
GET /_cat/nodes 查看所有节点
GET /_cat/health 查看es健康状况
GET /_cat/master 查看主节点
GET /_cat/indices 查看所有索引 show databases
2.添加数据
注:http://124.222.22.217:9200/customer/external/1
其中的 ‘1’ 指定了id,PUT 请求必须携带id;而 POST 可以不指定 id,不指定id,会自动生成id,指定id会对其进行修改(不存在则新增)
3.查询数据
GET customer/external/1
精确根据ID查找
GET customer/_search
查询所有
GET customer/_search 条件查询
{
"query":{"match_all":{}},
"sort":[
{"account_number":"asc"}
],
"from":10,
"size":10
}
http://124.222.22.217:9200/customer/external/1?if_seq_no=0&if_primary_term=1 # 修改配合并发使用
4.修改数据
POST携带JSON(带上doc) http://124.222.22.217:9200/customer/external/1/_update # 会检查前后更新内容是否一致,其余方式如PUT、POST(不带_update)都不会对比内容
5.删除数据
DELETE http://124.222.22.217:9200/customer/external/1
6.批量API
POST custmoer/external/_bulk
{"index":{"_id":1}}
{"name":"Jone"}
{"index":{"_id":"2"}}
{"name":"Jane"}
# 回车必要
进阶检索
1.SearchAPI
ES支持两种基本方式减缩:
- 一个是通过使用 REST request URI 发送搜索参数( uri + 检索参数)
- 另一个是通过使用 REST request body 来发送他们( uri + 请求体)
# 样例
GET bank/_search?q=*&sort=account_number:asc
-------
GET bank/_search
{
"query":{
"match_all":{
}
},
"sort":[{
"account_number":"asc"
},{
"balance":"desc"
}
]
}
请求体语法格式
{
QUERY_NAME{
ARGUMENT:VALUE,
ARGUMENT:VALUE
}
}
参数说明
# 一级参数
query 指定查询操作
sort 指定排序字段
from 分页操作
size 分页操作
_source 指定查询字段
# 二级参数
query:match { key:value } 非字符串值模糊查询(按相关度-score排序);数字则精确匹配
query:match_phrase 类似于前者,但不会对字符串进行分词,而是当做一条短语进行匹配
query:multi_match 分词 + 多字段匹配
query:bool 构造复杂查询 must must_not should(可以提高得分)
query:filter 不计算相关性得分,直接过滤
query:term term是代表完全匹配,即不进行分词器分析,文档中必须包含整个搜索的词汇;全文检索字段用 match ,其他 text 字段用term
query:aggregations 字段聚合处理
2.Mapping
指定索引下的属性类型
修改映射(仅限于添加)
那么如何修改?
数据迁移
elastic已经不推荐使用 type
3.分词
添加自定义词汇
- 配置 nginx 作为远程词库
- 在 html/es/fenci.txt 中填入新词
- elasticsearch/plugs/… 中配置远程词库为上面的地址
Redis 缓存
整合 Redis
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 调用API RedisTemplate StringRedisTemplate
- 代码整合示例
public Map<String, List<Catalog2Vo>> getCatalogJson(){
String catalogJSON = redis.opsForValue().get("catalogJson");
if(StringUtils.isEmpty(catalogJSON)){
Map<String, List<Catalog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
redis.opsForValue().set("catalogJson",
JSON.toJSONString(catalogJsonFromDB));
}
Map<String, List<Catalog2Vo>> list = JSON.parseObject(catalogJSON,
new TypeReference<Map<String, List<Catalog2Vo>>>(){
});
return list;
}
高并发下的缓存问题
缓存穿透
- 查询一个一定不存在的数据,由于缓存一定不命中,将去查询数据库,但数据库也无记录,这就失去了缓存的意义
- 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
- 解决
- null 结果缓存,并加入短暂过期时间
缓存雪崩
- 设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到了数据库,压力瞬时过大
- 解决
- 过期时间采用随机数
缓存击穿
- 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发的访问,是一种非常【热点】的数据
- 如果这个 key 在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,称为缓存击穿
- 解决
- 加锁;大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取锁,先查缓存,就会有数据,不用去db
加锁解决【缓存击穿】
该段代码存在分布式锁的问题
分布式锁简单实现
依托于redis的 set catalog_lock lockId [ex seconds][px millseconds] nx
命令实现
public Map<String, List<Catalog2Vo>> getCatalogJson() {
// double check
String catalogJSON = redis.opsForValue().get("catalogJson");
if (StringUtils.isEmpty(catalogJSON)) {
// 分布式加锁 ↓
String lockId = UUID.randomUUID().toString();
// set catalog_lock lockId [ex seconds][px millseconds] nx
if (Boolean.TRUE.equals(redis.opsForValue().
setIfAbsent("catalog_lock", lockId, 300L, TimeUnit.SECONDS))) {
// log.error("redis成功加锁!!!");
// 分布式加锁 ↑
//业务执行开始//
try {
Map<String, List<Catalog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
redis.opsForValue().set("catalogJson",
JSON.toJSONString(catalogJsonFromDB),
1, TimeUnit.DAYS);
} finally {
//业务执行结束,勿忘删除🔐//
// 防止业务执行时间过长,导致删除操作实际上删除的是别人的锁
// 但是这两步骤并不是原子操作,获取值进行比较的时候可能锁已经过期
// 所以需要采用 lua 脚本来保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redis.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList("lock"), lockId);
// log.error("redis成功删锁!!!");
}
} else {
// log.error("等待锁!!");
try {
// 防止空转
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJson();
}
}
return JSON.parseObject(catalogJSON,
new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
}
Redisson框架解决分布式锁
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.8</version>
</dependency>
- 配置类
@Configuration
public class RedissonConfig {
@Bean
RedissonClient redisson(){
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("L200107208017@./");
return Redisson.create(config);
}
}
Redisson 说明
- ReentrantLock
- ReadWriteLock
加锁示例
- Semaphore
计数器,正计时
- CountDownLatch
计数器,倒计时
分布式锁 Redisson 实现
/**
* 普通锁实现
*/
public Map<String, List<Catalog2Vo>> getCatalogJson() {
// 双重检查
String catalogJson = redis.opsForValue().get("catalogJson");
Map<String, List<Catalog2Vo>> catalogJsonFromDB;
if (StringUtils.isEmpty(catalogJson)) {
// lock
RLock catalogLock = redisson.getLock("catalogJsonLock");
catalogLock.lock();
try {
// 双重检查
catalogJson = redis.opsForValue().get("catalogJson");
if (StringUtils.isNotEmpty(catalogJson)) {
return JSON.parseObject(catalogJson,
new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
}
catalogJsonFromDB = getCatalogJsonFromDB();
} finally {
catalogLock.unlock();
}
return catalogJsonFromDB;
}else{
return JSON.parseObject(catalogJson,
new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
}
}
/**
* 读写锁实现
*/
public Map<String, List<Catalog2Vo>> getCatalogJson() {
RReadWriteLock readWriteL