1、读写分离
1、首先准备两台服务器,2个虚拟机做测试,分别是:
1.主库(master) 192.168.138.129 postgresql 11
2.从库(standby)192.168.138.128 postgresql 11
安装步骤参考
(默认已经初始化数据库并配置好远程访问)

2、配置主库 (192.168.138.129)

vim postgresql.conf(不清楚文件在哪可以使用 find / -name “postgresql.conf” 查找)

postgres双主模式 pgsql双主_postgres双主模式


wal_level:主从复制模式

max_wal_senders:需要设置为一个大于0的数,它表示主库最多可以有多少个并发的standby(从)数据库

wal_keep_segments:WAL日志文件个数

vim pg_hba.conf 修改pg_hba.conf文件:

host replication postgres 192.168.138.128/32 md5 (配置从库的ip)
这句话的意思允许从数据库连接主数据库去拖wal日志数据

3、配置从库 (192.168.138.128)
执行cd /var/lib/pgsql/11 进入从数据库的data文件夹
执行mv data data_back 备份数据
执行pg_basebackup -h 192.168.138.129 -U postgres -F p -P -R -D /var/lib/pgsql/11/data/
输入主数据库postgres 用户的密码

将主数据库的data目录同步过来,并生成recovery.conf恢复文件

在基础备份时,初始化文件是从主库复制来的,所以配置文件一致,

执行 vim postgresql.conf

注释掉wal_level 、max_wal_senders、wal_keep_segments

postgres双主模式 pgsql双主_服务器_02

打开如下参数:

postgres双主模式 pgsql双主_postgres双主模式_03


hot_standby = on 在备份的同时允许查询

max_standby_streaming_delay = 30s 可选,流复制最大延迟

wal_receiver_status_interval = 10s 可选,从向主报告状态的最大间隔时间

hot_standby_feedback = on 可选,查询冲突时向主反馈

给主从库配置权限
cd /var/lib/pgsql/11 进入/var/lib/pgsql/11 目录
chown -R postgres:postgres data 给postgres 用户data目录权限
chmod -R 0700 data 修改data目录权限

4、启动主从数据库
systemctl start postgresql-11.service

5、验证主从配置是否成功

systemctl status postgresql-11.service -l

(1)查看主库

postgres双主模式 pgsql双主_服务器_04


(2)查看从库

postgres双主模式 pgsql双主_spring_05

(3)在主库中创建表,看看从库是否同步

postgres双主模式 pgsql双主_服务器_06


(4)从库不可以修改,添加,删除数据

postgres双主模式 pgsql双主_postgres双主模式_07


2、使用Keepalived实现PGSQL数据库高可用

1、Keepalived的作用是检测服务器的状态,如果有一台服务器宕机,或工作出现故障,Keepalived将检测到,并将有故障的服务器从系统中剔除,同时使用其它服务器代替该服务器的工作,当服务器工作正常后Keepalived自动将服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的服务器。

2、环境和架构
192.168.138.129主库
192.168.138.128 从库
192.168.11.210 VIP (虚拟IP)

postgres双主模式 pgsql双主_postgres双主模式_08

注意:以下操作主从服务器一致
3、使用yum 源安装Keepalived
执行 yum install -y Keepalived 安装

4、修改keepalived.conf 配置文件,
执行 cd /ect/keepalived 进入/ect/keepalived 目录
修改keepalived.conf 权限,执行chomd 644 keepalived.conf
执行vim /ect/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
router_id PGSQL-HA #此处注意router_id为负载均衡标识,在局域网内应该是唯一的。
#notification_email { #指定keepalived在发生切换时需要发送email到的对象,一行一个
# qq@qq.com #指定收件人邮箱
#}
#notification_email_from Alexandre.Cassen@firewall.loc #指定发件人
#smtp_server 192.168.200.1 #指定smtp服务器地址
#smtp_connect_timeout 30 #指定smtp连接超时时间

}
vrrp_script check_pg_alived {# 检查pg数据库是否运行
script “/etc/keepalived/scripts/check_pg.sh”
interval 5 # 设置脚本执行的时间间隔,这里为每5秒执行一次
fall 3 # 3次失败认为数据库不在运行,会发送邮件到指定发件人

}

vrrp_instance VI_1 {
state BACKUP #状态只有MASTER和BACKUP
interface ens33 #通信所使用的网络接口
virtual_router_id 61 #虚拟路由的ID号
priority 100 #此节点的优先级,主节点的优先级需要比其他节点高
nopreempt #设置为不抢占 注:这个配置只能设置在backup主机上,而且这个主机优先级要比另外一台高
advert_int 1 #通告的间隔时间
authentication { #认证配置
auth_type PASS
auth_pass 1234
}

track_script {
    check_pg_alived # 配置业务进程监控脚本
}

virtual_ipaddress { # 虚拟ip
    192.168.138.111
}

}

5、创建文件夹scripts, log
执行mkdir scripts ,mkdir log, vim check_pg.sh
赋予运行权限 chomd 755 check_pg.sh
check_pg.sh脚本 – 判断pg是否还运行
#!/bin/bash

export PGDATABASE=postgres
export PGPORT=5432
export PGUSER=postgres
export PGPW=postgres
export PGHOME=/usr/pgsql-11/bin
export PATH=postgres双主模式 pgsql双主_spring_09PGHOME
export PGMIP=127.0.0.1
LOGFILE=/etc/keepalived/log/pg_status.log # 日志文件

SQL2=‘update sr_delay set sr_date = now() where id =1;’# 时间

SQL1=‘SELECT pg_is_in_recovery from pg_is_in_recovery();’# 判断数据库是主还是备 f 为主

SQL3=‘SELECT 1;’# 判断pg数据库是否运行

db_role=echo $SQL1 |PGPASSWORD=$PGPW $PGHOME/psql -d $PGDATABASE -U $PGUSER -At

if [ $db_role = ‘t’ ];then
echo -e date +"%F %T" “Attention1:the current database is standby DB!” >> $LOGFILE
exit 0
fi

备库不检查存活,主库更新状态

echo postgres双主模式 pgsql双主_postgresql_10PGPW psql -p $PGPORT -d $PGDATABASE -U $PGUSER -At

if [ $? -eq 0 ] ;then
echo $SQL2 | psql -p $PGPORT -d $PGDATABASE -U $PGUSER -At -w
echo -e date +"%F %T" “Success: update the master sr_delay successed!” >> $LOGFILE
exit 0
else
echo -e date +"%F %T" “Error:Is the server is running?” >> $LOGFILE
exit 1
fi

6、运行keepalived

执行systemctl start keepalived.service 启动程序,

执行systemctl enable keepalived.service 开机启动

执行systemctl status keepalived.service -l查看状态

postgres双主模式 pgsql双主_postgresql_11


注意:这里只是对数据库连接做负载,keepalived的简单使用,若想深入了解keepalived,还得keepalived论坛深造。3、Spring Data JPA动态数据源的使用

postgres双主模式 pgsql双主_postgres双主模式_12


2、数据源配置文件:application.properties。 可以支持不同数据库类型 例如mysql和pgsql

spring.datasource.master.url=jdbc:postgresql://192.168.138.129:5432/postgres
spring.datasource.master.username=postgres
spring.datasource.master.password=postgres
spring.datasource.master.driver-class-name=org.postgresql.Driver

#spring.datasource.node.url=jdbc:postgresql://192.168.138.128:5432/postgres
#spring.datasource.node.url=jdbc:postgresql://127.0.0.1:5432/postgres
#spring.datasource.node.username=postgres
#spring.datasource.node.password=postgres
#spring.datasource.node.driver-class-name=org.postgresql.Driver

spring.datasource.node.url=jdbc:mysql://localhost:3306/ssy?characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
spring.datasource.node.username=root
spring.datasource.node.password=123456
spring.datasource.node.driver-class-name=com.mysql.cj.jdbc.Driver

3、创建DynamicDataSource动态数据源 并继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @Description 动态数据源
 * AbstractRoutingDataSource(每执行一次数据库,动态获取DataSource)
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

查看AbstractRoutingDataSource源码得知 determineCurrentLookupKey方法是决定使用什么数据源

postgres双主模式 pgsql双主_spring_13


4、DataSourceConfig 创建多个数据源

(1)创建多个数据源

(2)将不同的数据源放入DynamicDataSource动态数据源类中

package com.redis.temp.kaiwen.conf;

import com.alibaba.druid.pool.DruidDataSource;
import com.redis.temp.kaiwen.jpa.DynamicDataSource;
import com.redis.temp.kaiwen.jpa.DynamicDataSourceContextHolder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: liangjinyin
 * @Date: 2020-11-09
 * @Description:
 */
@Configuration
public class DataSourceConfig {

    //master
    @Value("${spring.datasource.master.driver-class-name}")
    private String masterClassName;

    @Value("${spring.datasource.master.username}")
    private String masterUserName;

    @Value("${spring.datasource.master.password}")
    private String masterPassword;

    @Value("${spring.datasource.master.url}")
    private String masterUrl;

    //node
    @Value("${spring.datasource.node.driver-class-name}")
    private String nodeClassName;

    @Value("${spring.datasource.node.username}")
    private String nodeUserName;

    @Value("${spring.datasource.node.password}")
    private String nodePassword;

    @Value("${spring.datasource.node.url}")
    private String nodeUrl;

    @Bean(name = "masterDataSource")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().driverClassName(masterClassName)
                .password(masterPassword).url(masterUrl)
                .username(masterUserName).type(DruidDataSource.class).build();
    }

    @Bean(name = "nodeDataSource")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().driverClassName(nodeClassName)
                .password(nodePassword).url(nodeUrl)
                .username(nodeUserName).build();
    }

    @Bean(name = "multipleDataSource")
    @Primary
    public DataSource MultipleDataSourceToChoose() {
        DataSource master = DataSourceBuilder.create().driverClassName(masterClassName)
                .password(masterPassword).url(masterUrl)
                .username(masterUserName).type(DruidDataSource.class).build();


        DataSource node = DataSourceBuilder.create().driverClassName(nodeClassName)
                .password(nodePassword).url(nodeUrl)
                .username(nodeUserName).type(DruidDataSource.class).build();

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("masterDataSource", master);
        targetDataSources.put("nodeDataSource", node);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(master);
        DynamicDataSourceContextHolder.saveDataSourceTypeName("masterDataSource");
        DynamicDataSourceContextHolder.saveDataSourceTypeName("nodeDataSource");
        return dynamicDataSource;
    }

}

5、创建存储数据源信息的类

public class DynamicDataSourceContextHolder {
    /**
     * 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    /**
     * 使用setDataSourceType设置当前的
     * @param dataSourceType dataSourceType
     */
    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }
    /**
     * 获取数据源类型 默认使用主数据源
     * @return 数据源
     */
    public static String getDataSourceType() {
        return contextHolder.get() == null ? "masterDataSource" : contextHolder.get();
    }
    /**
     * 清除数据源
     */
    public static void clearDataSourceType() {
        contextHolder.remove();
    }


    public static void saveDataSourceTypeName(String name) {
        dataSourceList.add(name);
    }

    public static boolean checkDataSourceType(String name) {
        return dataSourceList.contains(name);
    }

    /**
     * 校验输入的数据库名称是否正确
     */
    private static List<String> dataSourceList = new ArrayList<>();
}

5、自定义注解 默认使用主数据源,并利用aop实现动态数据源切换。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ChangDataSource {

    String name() default "masterDataSource";
}

AOP切面

@Aspect
@Order(-10)//保证该AOP在@Transactional之前执行
@Component
@Slf4j
public class DynamicDataSourceAspect {


    @Before(value = "@annotation(source)")
    public void changeDataSource(JoinPoint point, ChangDataSource source) throws Exception {
        String name=source.name();
        if(!DynamicDataSourceContextHolder.checkDataSourceType(name)){
            throw new Exception("没有该数据源!");
        }
        DynamicDataSourceContextHolder.setDataSourceType(name);
    }

    @AfterReturning(value = "@annotation(source)")
    public void restoreDataSource(JoinPoint point,ChangDataSource source) {
        //方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。
        DynamicDataSourceContextHolder.clearDataSourceType();
    }

}

6、Java中的应用
(1)实体类

@Entity
@Data
@Table(name = "test_user")
public class User {
    @Id
    @GenericGenerator(name = "gid", strategy = "uuid")
    @GeneratedValue(generator = "id")
    @Column(name = "gid", length = 36, nullable = false, unique = true)
    private String id;
    @Column(name = "name", length = 50)
    private String name;
    @Column(name = "age")
    private int age;
}

(2)DAO层

@Repository
public interface TestDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {


}

(3)数据库表

postgres双主模式 pgsql双主_postgresql_14


(3)调用,在方法上加上自定义注解,写上要操作的数据源名称,默认是主数据源可以不写

@RestController
@RequestMapping("/jpa")
@Slf4j
public class JpaTestController {

    @Resource
    private TestDao testDao;

    @Resource
    private Test1Dao test1Dao;

    @GetMapping("/aa")
    @ChangDataSource(name = "nodeDataSource")
    public String getTest() {
        List<User> all = testDao.findAll();
        return all.toString();
    }