亚信偷鸡
Redis实现分布式锁(通过Jedis)
保证四个条件
互斥性
任意时刻,只有一个客户端能持有锁
容错性
大部分Redis节点正常运行时,客户端就可以加锁和解锁
不会发生死锁
即使有一个客户端在持有锁期间没有主动解锁,也能保证后续其他客户端能加锁
解铃还须系铃人
谁加的谁释放
代码实现
组件依赖
引入jedis依赖
`<dependency>`
`<groupId>redis.clients</groupId>`
`<artifactId>jedis</artifactId>`
`<version>2.9.0</version>`
`</dependency>`
加锁代码
`public class RedisTool{`
`private static final string LocK_sUccEss`
`private static final String SET IF NOTXIST = "NX";`
`private static final String SET WITH EXPIRE TIME = "PX";`
`/**`
`*尝试获取分布式锁`
`*@param jedis Redis客户端`
`@param lockKey 锁`
`@param requestId 请求标识`
`*@param expireTime 超期时间`
`*@return 是否获取成功`
`米`
`public static boolean tryGetDistributedLock(Jedis jedis, string lockKey, string requestId, int expireTime){`
`String result = jedis.set(lockKey, requeStId, SET IF NOT EXIST, SET WITH EXPIRE TIME,expireTime);if(LOCK SUCCEss.equals(result)){`
`return true;}`
`return false;}`
通过锁的唯一标识,和SET IF NOT EXIST,以及SET WITH EXPIRE TIME来保证在无锁时加锁并设置过期时间,这里因为只考虑了单机部署,所以使用requestID做了唯一标识,如果多机部署,可以考虑实例ID
常见错误加锁示例
使用jedis.setnx()和jedis.expire()加锁
public static void wrongGetLock1(Jedis jedis, string lockKey, string requestId, int expireTime)
{ Long result=jedis.setnx(lockKey,requestId);
if(result ==1){ //若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey,expireTime);}
用sentx()实现了原子加锁,expire()加上过期时间,但这是两条redis命令,所以就没有保证原子性
使用sentx加锁,没有则加,有则比较过期时间
`public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime)`
`{`
`long expires=System.currentTimeMillis()+expireTime;`
`String expiresStr=String.value0f(expires);`
`//如果当前锁不存在,返回加锁成功`
`if(jedis.setnx(lockKey,expiresStr)==1){`
`return true;`
`//如果锁存在,获取锁的过期时间`
`string currentValuestr = jedis.get(lockkey)`
`if (currentValueStr != null &&Long.parseong(currentValuestr)<system.currentTimeMillis()){//锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间String oldValueStr=jedis.getSet(lockKey,expiresStr);`
`if(oldValueStr != null && oldValueStr.equals(currentValueStr)){`
`//考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁return true;`
`//其他情况,一律返回加锁失败`
`return false;}`
1.客户端自己生成过期时间,那每个实例的时间都应一致
2.锁过期,使用getSet方法会产生过期时间覆盖的问题
3.锁不具备拥有者标识,任何人都可以释放锁
解锁代码
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁的key
* @param requestId 请求唯一标识(需与加锁时的值一致)
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
// Lua脚本(原子性验证锁值并删除)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ " return redis.call('del', KEYS[1]) "
+ " return 0 "
// 执行脚本(包含1个键、1个参数)
Object result = jedis.eval(
script,
Collections.singletonList(lockKey),
Collections.singletonList(requestId)
);
return RELEASE_SUCCESS.equals(result);
}
解锁错误示例
直接使用jedis.del解锁
public static void wrongReleaseLock1(Jedis jedis, String lockKey)
{
jedis.del(lockKey);}
错误在没有判度锁是谁的,谁都可以释放锁
使用两条命令去解锁
public static void wrongReleaseLock2(Jedis jedis, string lockKey, string requestId){
//判断加锁与解锁是不是同一个客户端
if(requestId.equals(jedis.get(lockKey))){
//若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
redis的原子性,get和del是两条指令,这样会导致误解锁
总结
以上只是考虑了单机情况下,主要核心在原子操作lua脚本和先判断再解锁,多机部署可以使用Redission。
Docker实践
两项支持容器的技术储备
cgroup
控制资源使用量,由goole贡献
namespaces
隔离环境
Docker容器
容器能力
提供隔离运行环境
不同容器间的应用不能通信,容器内应用不能与宿主机应用通信
受到的资源限制
cpu计算资源
内存资源
磁盘I/O资源
镜像
什么是镜像
每个下拉镜像有两个属性
-repository
-tag 镜像tag
镜像实现
多层封装
容器与镜像的交互
编写Dockerfile
一、基础指令解析
- FROM
指定基础镜像,必须为第一条指令。建议优先使用官方镜像以减少安全风险。FROM node:18-alpine # 使用Node.js官方镜像的轻量版本 - RUN
在镜像构建过程中执行命令,常用于安装依赖或配置环境。合并多条命令以减少镜像层数:RUN apt-get update && apt-get install -y \ git \ python3 \ && rm -rf /var/lib/apt/lists/* # 清理缓存减小镜像体积 - COPY与ADD
•COPY用于复制本地文件到镜像中(推荐优先使用)•
ADD支持自动解压压缩包和远程URL(慎用远程资源)COPY package.json yarn.lock /app/ # 仅复制依赖文件 ADD https://example.com/data.tar.gz /tmp/ # 自动解压tar文件 - WORKDIR
设置容器内的工作目录,后续命令均在此目录执行。避免使用RUN cd:WORKDIR /app # 后续操作默认在/app目录下执行 - CMD与ENTRYPOINT
•CMD定义容器启动时的默认命令(可被docker run覆盖)•
ENTRYPOINT设置主进程命令(常与CMD配合传参)ENTRYPOINT ["java", "-jar"] CMD ["app.jar"] # 实际命令为 java -jar app.jar
二、高级配置指令
- ENV与ARG
•ENV设置容器内的环境变量(运行时生效)•
ARG定义构建时的临时变量(构建结束后失效)ARG APP_VERSION=1.0 ENV VERSION=$APP_VERSION # 将构建参数转为环境变量 - EXPOSE
声明容器监听的端口(需通过-p参数映射到宿主机):EXPOSE 3000 # 提示用户此端口需映射 - USER
指定运行容器的用户(增强安全性,避免root权限):USER node # 使用非特权用户
三、优化技巧与最佳实践
- 减少镜像体积
• 使用多阶段构建(分离编译环境与运行环境):# 第一阶段:编译 FROM node:18 AS build WORKDIR /app COPY . . RUN npm install && npm run build # 第二阶段:运行 FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html - 加速构建过程
• 合理利用缓存:将频繁变动的指令(如COPY . .)放在文件末尾• 使用
.dockerignore排除无关文件(如node_modules)
- 安全性增强
• 定期更新基础镜像版本(修复CVE漏洞)• 避免在镜像中存储敏感信息(如私钥)
四、完整示例模板
# 多阶段构建示例
# 阶段1:构建
FROM golang:1.20 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app .
# 阶段2:运行
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app /app
COPY config.yaml ./
EXPOSE 8080
USER 1001
CMD ["./app"]
镜像仓库
Docker Registry
实现Docker镜像的全局存储
提供API接口
提供Docker镜像的下载/推送/查询
DockerHub
Docker官方仓库
本地开发实践
状态问题
状态指程序中需要维护的上下文信息,它通常由程序存储并被后续程序使用
- 在之前的开发模式中,开发者通常喜欢将程序运行的状态保存在程序自身的数据结构中,这些状态包括HTTP的session、程序暂存的数据、程序的日志等。如果一个程序是这样实现的,我们将其称之为有状态的程序(stateful)但如果一个程序在设计中就将“状态”保存在程序之外,比如通过外部的缓存数据库或者消息总线来保存,那么这样的程序被称为无状态的程序(statless)
- Docker偏爱无状态。有状态影响可用和可扩展性。有状态通过volume实现。
无状态应用是Docker的最佳实践
- 配置不通过文件,利用环境变量实现
- 日志不输出文件,输出到stdout和stderr
容器进阶
容器网络概览
- 与外界建立通信
- 单机网络模式
桥接模式(使用最广,默认模式)
主机模式(使用主机网络)
容器模式(共享其他容器网络栈)
- 集群网络模式(overlay 通过libnetwork插件)
- 隔离的网络栈
网桥模式
主机模式
利用其他容器
None模式
Docker集群网络
容器存储
容器编排
link命令和compose文件
容器日志
- docker logs 命令
- 使用docker exec进入容器查看
- 配置 log-driver 输出到外部系统
- 使用docker-compose logs查看日志
- 集中化日志管理工具 ELK(ES+Logstash+Kibana,Promethus+Grafana)
DevOps
微服务
单体模式的不足
- 应用工程变得又大又复杂
- 敏捷开发和部署举步维艰
- 启动时间长
- 可靠性差
- 难以采用新技术新语言






