潘逸安的博客

潘逸安

docker创建redis集群

创建6台redis

1
2
3
4
5
6
docker run -d --name redis-node-1 --net host --privileged=true -v /data/redis/share/redis-node-1:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6381
docker run -d --name redis-node-2 --net host --privileged=true -v /data/redis/share/redis-node-2:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6382
docker run -d --name redis-node-3 --net host --privileged=true -v /data/redis/share/redis-node-3:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6383
docker run -d --name redis-node-4 --net host --privileged=true -v /data/redis/share/redis-node-4:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6384
docker run -d --name redis-node-5 --net host --privileged=true -v /data/redis/share/redis-node-5:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6385
docker run -d --name redis-node-6 --net host --privileged=true -v /data/redis/share/redis-node-6:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6386

选择一个容器进入

1
docker exec -it redis-node-1 /bin/bash

创建集群

1
2
3
4
5
6
7
8
redis-cli --cluster create \
192.168.10.10:6381 \
192.168.10.10:6382 \
192.168.10.10:6383 \
192.168.10.10:6384 \
192.168.10.10:6385 \
192.168.10.10:6386 \
--cluster-replicas 1

输入 yes

image-20230317115538759

选择进入一个redis

1
redis-cli -p 6381

查看集群信息

1
cluster info

image-20230317115743540

查看集群节点

1
cluster nodes

image-20230317115807093

对应关系每次随机分配

1 — 5

2 — 4

3 — 6

集群操作读写

增加参数 -c,随便访问集群哪个节点

1
redis-cli -p 6381 -c

会自动切换到指定节点

1
set k1 v1

image-20230317142051073

1
get k2

image-20230317142111444

集群扩容

添加主节点

创建 6387

1
docker run -d --name redis-node-7 --net host --privileged=true -v /data/redis/share/redis-node-7:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6387

加入集群,作为master

1
redis-cli --cluster add-node 192.168.10.10:6387 192.168.10.10:6381

查看集群的状态

1
redis-cli --cluster check 192.168.10.10:6381

image-20230317142545802

没有槽好,需要分配

1
redis-cli --cluster reshard 192.168.10.10:6381

image-20230317142742391

填入 16383 / master 台数

第二个 new id 为 6387 的 id

重新检查 集群槽号

1
redis-cli --cluster check 192.168.10.10:6387

image-20230317143214538

结论: 重新分配的方式为,之前的所有节点,每个分配一部分给新的节点

添加从节点

1
docker run -d --name redis-node-8 --net host --privileged=true -v /data/redis/share/redis-node-8:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6388

作为6387的从节点加入集群

1
redis-cli --cluster add-node 192.168.10.10:6388 192.168.10.10:6387 --cluster-slave --cluster-master-id da57cde32e8696825235c6fe8ec80fc8cc3699d8

查看节点

1
cluster nodes

image-20230317143741822

集群缩容

先删除从节点,再删除主节点

先删除 6388 后面为节点的id

1
redis-cli --cluster del-node 192.168.10.10:6388 d520138203258effc8e0a5e92df579bfaa48266a

先重新分配 6387 的槽号

1
redis-cli --cluster reshard 192.168.10.10:6381

image-20230317145344757

检查集群情况

1
redis-cli --cluster check 192.168.10.10:6381

image-20230317145509252

6387 的全给 6381了

1
redis-cli --cluster del-node 192.168.10.10:6387 da57cde32e8696825235c6fe8ec80fc8cc3699d8

image-20230317145556210

1
redis-cli --cluster check 192.168.10.10:6381

image-20230317145625159

docker创建xxl-job-admin

拉取镜像

1
docker pull xuxueli/xxl-job-admin:latest

创建并运行容器

如果mysql在docker中,可以通过宿主机连接

1
2
3
4
5
6
docker run -d \
-e PARAMS="--spring.datasource.url=jdbc:mysql://172.17.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.password=123456" \
-p 8196:8080 \
-v /home/pan/docker/xxl-job/logs:/data/applogs \
--name xxl-job-admin \
xxl-job-admin:latest

登录

登录 http://127.0.0.1:8196/xxl-job-admin

账号:admin

密码:123456

docker-maven中打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<properties>

<!-- 推荐使用Harbor -->
<docker.registry.url>registry.cn-hangzhou.aliyuncs.com</docker.registry.url>
<docker.username>*****</docker.username>
<docker.password>*****</docker.password>
<docker.namespace>pandev</docker.namespace>
<docker.plugin.version>1.4.13</docker.plugin.version>

</properties>

<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker.fabric.version}</version>
<configuration>
<skip>true</skip>
<authConfig>
<username>${docker.username}</username>
<password>${docker.password}</password>
</authConfig>
<registry>${docker.registry.url}</registry>
<images>
<image>
<name>${docker.namespace}/${project.build.finalName}:${project.version}</name>
<alias>${project.name}</alias>
<build>
<dockerFile>${project.basedir}/Dockerfile</dockerFile>
</build>
</image>
</images>
</configuration>
</plugin>

docker例子

portainer

1
docker run -d -p 9000:9000 --restart=always -v /var/run/docker.sock:/var/run/docker.sock --name prtainer-test portainer/portainer

nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#安装nginx 容器
docker pull nginx:1.10
创建一个测试容器,用来复制配置文件
docker run -d --name nginx_test nginx:1.10
#复制配置文件
docker container cp nginx_test:/etc/nginx .
#修改目录
mv nginx conf
#创建容器
docker run -p 80:80 --privileged=true --name nginx \
-v /home/nginx/html:/usr/share/nginx/html \
-v /home/nginx/logs:/var/log/nginx \
-v /home/nginx/conf:/etc/nginx \
-d nginx:1.10

docker run -p 80:80 --privileged=true --name nginx \
-v /home/nginx/html:/usr/share/nginx/html \
-v /home/nginx/logs:/var/log/nginx \
-v /home/nginx/conf:/etc/nginx \
-d nginx:1.10

docker run -p 80:80 --privileged=true --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

mysql

Linux系统

1
2
3
4
5
6
7
docker run -p 3306:3306 --name mysql \
-v /Users/bxs/docker/mysql/log:/var/log/mysql \
-v /Users/bxs/docker/mysql/data:/var/lib/mysql \
-v /Users/bxs/docker/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=rootroot \
--privileged=true \
-d mysql:5.7

mac系统

1
2
3
4
5
6
7
docker run -p 3306:3306 --name mysql \
-v /Users/bxs/docker/mysql/log:/var/log/mysql \
-v /Users/bxs/docker/mysql/data:/var/lib/mysql \
-v /Users/bxs/docker/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=rootroot \
-u root \
-d mysql:5.7

/mydata/mysql/conf/my.cnf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

[mysqld]
init_connect='SET collation_connection = utf8mb4_general_ci'
init_connect='SET NAMES utf8mb4'
#服务器默认编码格式
character-set-server=utf8mb4
#服务器默认字符集
collation-server=utf8mb4_general_ci
# 跳过mysql程序起动时的字符参数设置 ,使用服务器端字符集设置
skip-character-set-client-handshake
# 跳过域名解析,只能使用127.0.0.1
skip-name-resolve
1
2
3
4
5
6
7
docker run -p 3306:3306 --name mysql \
-v /Users/pan/docker/mysql/log:/var/log/mysql \
-v /Users/pan/docker/mysql/data:/var/lib/mysql \
-v /Users/pan/docker/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=rootroot \
-u root \
-d mysql:8

spring boot

创建一个spring boot 项目

打包 mvn clean package

创建Dockerfile文件

1
2
3
4
FROM java:openjdk-8-jre-alpine
WORKDIR /home
COPY ./*.jar /home
ENTRYPOINT ["java", "-jar", "*.jar"]

构建镜像

1
docker buildx build --tag demo:1.0 .

创建并运行容器

1
docker run -d --name demo -p 8080:8080 demo:1.0

PHP7.4-FPM

1
docker pull php:7.4-fpm

创建容器

1
2
3
docker run -p 9000:9000 --name  php74fpm 
-v /Users/pan/docker/nginx/html:/usr/share/nginx/html
--privileged=true -d php:7.4-fpm

注意事项,需要加入–privileged=true,让容器能访问外部文件

bitwarden

1
docker run -d --name bitwarden -p 8002:80 -v /root/data/docker_data/bitwarden/:/data/ vaultwarden/server:1.27.0-alpine

RabbitMQ

1
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 -d rabbitmq:management

redis

1
2
mkdir -p /root/redis/conf
touch /root/redis/conf/redis.conf
1
2
3
4
5
docker run -p 6379:6379 --name redis \
-v /root/redis/data:/data \
-v /root/redis/conf/redis.conf:/etc/redis/redis.conf \
--restart=always \
-d redis redis-server /etc/redis/redis.conf
1
docker run -p 6379:6379 --name redis -d redis redis-server

elasticsearch

1
2
3
4
5
6
7
8
9
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
echo "network.host: 0.0.0.0">>/mydata/elasticsearch/config/elasticsearch.yml

echo "network.host: 0.0.0.0">>config/elasticsearch.yml

echo "network.host: 0.0.0.0" >> /Users/pan/docker/elasticsearch/config/elasticsearch.yml

Chmod -R 777 /mydata/elasticsearch/ 保证权限问题
1
2
3
4
5
6
7
8
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e Es_JAVA_OPTS="-Xms65m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
--privileged=true \
-d elasticsearch:7.4.2
1
2
3
4
5
6
7
8
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e Es_JAVA_OPTS="-Xms65m -Xmx128m" \
-v /home/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /home/elasticsearch/data:/usr/share/elasticsearch/data \
-v /home/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
--privileged=true \
-d registry.cn-hangzhou.aliyuncs.com/panyian/elasticsearch:7.4.2

kibana

ELASTICSEARCH_HOSTS的参数为elasticsearch的地址

1
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.10.10:9200 -p 5601:5601 -d kibana:7.4.2

/usr/share/kibana/config/kibana.yml

1
2
3
4
server.host: "0"
server.shutdownTimeout: "5s"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
monitoring.ui.container.elasticsearch.enabled: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
docker run --name kibana \
-v /Users/pan/docker/kibana/kibana.yml:/usr/share/kibana/config/kibana.yml \
-e ELASTICSEARCH_HOSTS=http://192.168.10.10:9200 \
-p 5601:5601 \
-d kibana:7.14.2

docker run --name kibana \
-v /Users/pan/docker/kibana/kibana.yml:/usr/share/kibana/config/kibana.yml \
-p 5601:5601 \
-d kibana:7.14.2

docker run --name kibana7142 \
-e ELASTICSEARCH_HOSTS=http://172.17.0.2:9200 \
-p 5601:5601 \
-d kibana:7.14.2

docker run --name kibana762 \
-v /Users/pan/docker/kibana762/config/kibana.yml:/usr/share/kibana/config/kibana.yml \
-e ELASTICSEARCH_HOSTS=http://172.17.0.2:9200 \
-p 5601:5601 \
-d kibana:7.6.2
1
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://127.0.0.1:9200 -p 5601:5601 -d kibana:7.4.2

SpringBoot自定义注解实现Token校验

1.定义Token的注解,需要Token校验的接口,方法上加上此注解

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Token {
boolean validate() default true;
}

2.定义LoginUser注解,此注解加在参数上,用在需要从token里获取的用户信息的地方

1
2
3
4
5
6
7
8
9
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}

3.权限的校验拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
mport com.example.demo.annotation.Token;
import com.example.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
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;

@Component
@Slf4j
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
public static final String USER_KEY = "USER_ID";
public static final String USER_INFO = "USER_INFO";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Token annotation;
if(handler instanceof HandlerMethod) {
annotation = ((HandlerMethod) handler).getMethodAnnotation(Token.class);
}else{
return true;
}
//没有声明需要权限,或者声明不验证权限
if(annotation == null || annotation.validate() == false){
return true;
}
//从header中获取token
String token = request.getHeader("token");
if(token == null){
log.info("缺少token,拒绝访问");
return false;
}

//查询token信息
// User user = redisUtils.get(USER_INFO+token,User.class);
// if(user == null){
// log.info("token不正确,拒绝访问");
// return false;
// }

//token校验通过,将用户信息放在request中,供需要用user信息的接口里从token取数据
request.setAttribute(USER_KEY, "123456");
User user=new User();
user.setId(10000L);
user.setUserName("2118724165@qq.com");
user.setPhoneNumber("15702911111");
user.setToken(token);
request.setAttribute(USER_INFO, user);
return true;
}
}

4.写参数的解析器,将登陆用户对象注入到接口里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import com.example.demo.annotation.LoginUser;
import com.example.demo.entity.User;
import com.example.demo.interceptor.AuthorizationInterceptor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver
{
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().isAssignableFrom(User.class)&&methodParameter.hasParameterAnnotation(LoginUser.class);
}

@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
//获取登陆用户信息
Object object = nativeWebRequest.getAttribute(AuthorizationInterceptor.USER_INFO, RequestAttributes.SCOPE_REQUEST);
if(object == null){
return null;
}
return (User)object;
}
}

5.配置拦截器和参数解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.example.demo.interceptor.AuthorizationInterceptor;
import com.example.demo.resolver.LoginUserHandlerMethodArgumentResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthorizationInterceptor authorizationInterceptor;
@Autowired
private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/api/**");
}

@Override
public void addArgumentResolvers(List argumentResolvers) {
argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
}
}

7.测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.example.demo.annotation.LoginUser;
import com.example.demo.annotation.Token;
import com.example.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/api")
@Slf4j
public class TestController {
@RequestMapping(value="/test",method = RequestMethod.POST)
@Token
public String test(@LoginUser User user){
System.out.println("需要token才可以访问,呵呵……");
log.info("user:"+user.toString());
return "test";
}
@RequestMapping(value="/noToken",method = RequestMethod.POST)
public String noToken(){
System.out.println("不用token就可以访问……");
return "test";
}
}

至此,自定义注解实现token校验就大功告成了。

spring boot 格式化bigdecimal为小数点2位字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.piian.common.utils;


import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.math.BigDecimal;

public class TwoDecimalPlacesSerialize extends JsonSerializer<BigDecimal> {

@Override
public void serialize(BigDecimal bigDecimal, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (bigDecimal != null) {
jsonGenerator.writeString(bigDecimal.setScale(2, BigDecimal.ROUND_HALF_DOWN) + "");
} else {
jsonGenerator.writeString("");
}
}
}
1
2
3
4
5
6
@Data
public class OrderCommissionVo {
@JsonSerialize(using = TwoDecimalPlacesSerialize.class)
private BigDecimal total;
}

全局使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
@Value("${spring.jackson.date-format}")
String format;
@Bean
Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// 全局bigdecimal类型的转换
builder.serializerByType(BigDecimal.class, new CustomDecimalSerializer());
// 时间转换,通过设置环境
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(format)));
};
}
}

nginx搭建websocket服务器与客户端,websocket 从入门到放弃

1
2
3
4
5
6
7
8
9
10
server{
listen 80;
server_name wss.test;
location / {
proxy_pass http://127.0.0.1:9501;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

websocket_server
守护进程方式运行 nohup php artisan swoole:server &

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SocketServer extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'swoole:server';

/**
* The console command description.
*
* @var string
*/
protected $description = 'swoole服务端,用于开单的消息转发';

/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//创建websocket服务器对象,监听0.0.0.0:9502端口
$ws = new \Swoole\WebSocket\Server("127.0.0.1", 9501);
//监听WebSocket连接打开事件
$ws->on('open', function ($ws, $request) {
logger()->channel('wss_open')->info($request->fd . ' socket 连接');
$get = $request->get;
if ($get) {
foreach ($get as $key => $value) {
logger()->channel('wss_open')->info($value);
$this->setFid($value, $request->fd);
}
}
$ws->push($request->fd, json_encode(['status' => 1, 'message' => '连接成功'], 256));
});

//监听WebSocket消息事件
$ws->on('message', function ($ws, $frame) {
$data = json_decode($frame->data, true);
logger()->channel('wss_message')->info($frame->fd . ' socket 连接', $data);
if ($data) {
$su = $this->getFid($data['to']);
if ($ws->exist($su)) {
logger()->channel('wss_message')->info('发送成功');
$ws->push($su, json_encode([
'data' => $data['data'],
'status' => 1
], 256));
} else {
logger()->channel('wss_message')->info('发送失败');
$ws->push($frame->fd, json_encode(['status' => 0, 'message' => '找不到目标连接'], 256));
}
}
});

//监听WebSocket连接关闭事件
$ws->on('close', function ($ws, $fd) {
$this->delFid($fd);
});
$ws->start();
}

public function setFid($uid, $fid)
{
$client = new \Predis\Client();
$client->hset('uid', $uid, $fid);
}

public function getFid($uid)
{
$client = new \Predis\Client();
return $client->hget('uid', $uid);
}

public function delFid($fid)
{
$client = new \Predis\Client();
$uids = $client->hgetall('uid');
foreach ($uids as $uid => $fd) {
if ($fid == $fd) {
$client->hdel('uid', $uid);
echo '删除' . $uid;
}
}
}
}

前端 client

1
2
3
4
5
6
7
8
9
10
11
12
13
 const socket = new WebSocket('ws://wss.test?uid=customOrder.cashier.2008');

// Connection opened
socket.addEventListener('open', function (event) {
app.start_message = 'Hello Server!'
socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
app.messages.push(event.data)
console.log('Message from server ', event.data);
});

php-fpm client 基于 textalk/websocket

1
2
$client = new  \WebSocket\Client($uri);
$client->send(json_encode(['data' => $data, 'to' => $uid]));

Supervisor配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[program:flask]
# [program:程序名]
command=python /Users/pan/code/python/learn_flask/app.py ; 程序启动命令
startsecs=10 ; 启动10秒后没有异常退出,就表示进程正常启动了,默认为1秒,非必填
autorestart=true ; 程序退出后自动重启,可选值:[unexpected,true,false],默认为unexpected,表示进程意外杀死后才重启,true表示无条件重启
redirect_stderr=true ; 把stderr重定向到stdout,默认false
stdout_logfile_maxbytes=20MB ; stdout 日志文件大小,默认50MB
stdout_logfile_backups = 20 ; stdout 日志文件备份数,默认是10
; stdout 日志文件,需要注意当指定目录不存在时无法正常启动,所以需要手动创建目录(supervisord 会自动创建日志文件)
stdout_logfile=/Users/pan/logs/flask.out


;process_name= ;进程名称,默认 %(program_name)s 非必填
;numprocs= ;进程数,默认1,非必填
;numprocs_start= ;启动进程数,默认0,非必填
;priority=999 ; 进程启动优先级,默认999,值小的优先启动,非必填
;autostart=true ; 在supervisord启动的时候也自动启动,默认true,非必填
;startretries=3 ; 启动失败自动重试次数,默认是3,非必填
;user=pan ; 用哪个用户启动进程,默认是root
;stopsignal= ; 请求停止时用来终止程序的信号。这可以是TERM(终止)、HUP(平滑启动)、INT(退出)、QUIT(退出)、KILL(强制终止)、USR1或USR2中的任意一个。默认TERM,非必填
;stopasgroup=false ;默认为false,进程被杀死时,是否向这个进程组发送stop信号,包括子进程,非必须设置
;killasgroup=false ;默认为false,向进程组发送kill信号,包括子进程,非必须设置
;redirect_stderr=true ; 如果为true,则stderr的日志会被写入stdout日志文件中默认为false,非必须设置
;stdout_logfile=/a/path ; 子进程的stdout的日志路径,可以指定路径,AUTO,none等三个选项。设置为none的话,将没有日志产生。设置为AUTO的话,将随机找一个地方生成日志文件,而且当supervisord重新启动的时候,以前的日志文件会被清空。当 redirect_stderr=true的时候,sterr也会写进这个志文件
;stdout_logfile_maxbytes=1MB ; 日志文件最大大小,和[supervisord]中定义的一样。默认为50
;stdout_logfile_backups=10 ; 和[supervisord]定义的一样。默认10
;stdout_capture_maxbytes=1MB ; 这个东西是设定capture管道的大小,当值不为0的时候,子进程可以从stdout发送信息,而supervisor可以根据信息,发送相应的event。默认为0,为0的时候表达关闭管道。。。非必须项
;stdout_events_enabled=false ; 当设置为ture的时候,当子进程由stdout向文件描述符中写日志的时候,将触发supervisord发送PROCESS_LOG_STDOUT类型的event默认为false。。。非必须设置

;stderr_logfile=/a/path ; 这个东西是设置stderr写的日志路径,当redirect_stderr=true。这个就不用设置了,设置了也是白搭。因为它会被写入stdout_logfile的同一个文件中默认为AUTO,也就是随便找个地存,supervisord重启被清空。。非必须设置
;stderr_logfile_maxbytes=1MB ; 这个出现好几次了,就不重复了
;stderr_logfile_backups=10 ; 这个也是
;stderr_capture_maxbytes=1MB ; 这个一样,和stdout_capture一样。 默认为0,关闭状态
;stderr_events_enabled=false ; 这个也是一样,默认为false

;environment= ; 子进程环境变量
;serverurl= ; 子项目 UNIX socket路径
1
2
3
4
5
6
7
8
[program:da_tang_yun]
# [program:程序名]
command=php artisan queue:work redis ; 程序启动命令
directory=/home/wwwroot/da_tang_yun
startsecs=10 ; 启动10秒后没有异常退出,就表示进程正常启动了,默认为1秒,非必填
autorestart=true ; 程序退出后自动重启,可选值:[unexpected,true,false],默认为unexpected,表示进程意外杀死后才重启,true表示无条件重启
redirect_stderr=true ; 把stderr重定向到stdout,默认false
stdout_logfile=/home/wwwlogs/supervisor/da_tang_yun.log

redis事务机制实现秒杀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$client = new \Predis\Client();
// 监视键
$client->watch('sale_goods_1');
// 获取键
$res = $client->get('sale_goods_1');
$count = 3;

if ($res >= $count) {
dump($res);
exit('结束');
} else {
// 开始事务
$client->multi();
// 自增销量
$client->incr('sale_goods_1');

// 发送更新销量到队列


// 提交事务
$client->exec();
return '成功';
}