三种分布式事务LCN、Seata、MQ

分布式事务存在的原因:数据分布在不同的数据库实例。一个分布式系统下存在多个模块协调来完成一次业务,每一个模块对应一个数据源,同一个业务需要操作不同的模块,改动不同的数据库,要么都成功,要么都失败。

举例:

去A库中存储老师实体类的数据

@Data
public class Teacher{
    @Id
    private Integer tid;
    @Field
    private String tname;
}

去B库中存储学生实体类的数据

@Data
@Document
public class Teacher{
    @Id
     private String id;
    @Field
     privtae String name;
     @Field
     private Integer tid;
}

这两个pojo类通过tid关联,学生对老师是一对多的关系,两个POJO的数据通过mysql存储在两个不同的类。

常见的分布式框架解决方案

  • 全局事务2pc --关系型数据库

    缺点:效率低,算法执行过程中,所有的节点处于阻塞状态,所有节点所持有的资源处于封锁状态。

  • 3pc(三段提交协议)

  • 消息中间件

  • 提供回滚接口

为了方便我们了解分布式事务框架,我们需要先了解的相关说明。

Spring本地事务是在同一个系统里做的实务操作,需要符合数据库事务ACID特性,为刚性事务;而分布式事务往往不能满足ACID,不同的数据库实例对同一个方法在一个时间的响应并不完全一样,但可能最终一致,这种事务是柔性事务。

柔性事务定理了解

1. CAP定理

  一个分布式系统于事务由三个特性:Consistency(一致性) 、Availability(可用性)、 Partition tolerance(分区容错性)。现实情况下,分布式系统最多同时满足其中两个,不能同时满足三个。

 

 

  •   可用性和一致性是矛盾的,原因是在分布式事务中操作多个节点的时候,一个节点坏了还得有别的节点来承担,这是为了满足可用性,但不符合一致性。
  •   分区容错性意思是,系统不能在一定时限内达到数据的一致性,就意味着发生了分区的情况。假设在一定时间内,A节点和C节点没能同步数据,那么框架需要决定到底是A版本还是C版本作为后续操作数据的基版。

2. BASE理论

CAP理论的优化。Basically Available(基本可用 )、soft state(软状态),Eventually consistent(最终一致性)三个短语的缩写。

 

 

 

BASE理论要求,即使无法做到节点强一致,但每个分布式应用都可以根据自身业务特点,采用适当的方式达到最终一致性。

  • BA基本可用:指分布式系统中出现不可知故障的时候,允许损失部分可用性,但不代表整个系统不可用。(比如秒杀活动的并发降级页面)
  • S软状态:系统中数据允许存在中间状态(软状态),并认为这个状态不影响可用性,允许分布式节点之间存在同步延迟。(如Eureka集群同步数据)
  • 最终一致性:允许整个系统数据在经过一定时间后,最终能达到整个系统的一致性。为弱一致性,响应给用户结果时整个系统没有达到一致性,但最终一定会达到一致性的。

*强一致性:系统接受请求后,整个系统必须达到一致的结果才能响应。

根据不同的解决方案衍生出了不同的分布式事务技术,下面介绍不同的分布式技术在这个示例的表现。

LCN

介绍

XA两阶段提交协议

  • 第一阶段所有的参与者TC(Transaction Client)准备执行事务并锁住需要的资源,并向事务管理器报备自己已经准备好。
  • 第二阶段事务管理器负责协调,向不同的RM(资源管理器)索要数据,尽可能晚提交事务(在提交之前完成所有的工作),但也要等待TM(Transaction Manager)响应容易阻塞。吞吐量优先,不适合互联网。

三种模式

基于XA两阶段提交协议,现存LCN分布式事务框架有三种模式,分别是LCN模式,TCC模式,TXC模式。

1. LCN模式:基于jdbc关系型数据库实现对本地事务的操作,然后在由TM(Transaction Manager)统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。基于注解 @LcnTransaction。

2. TCC模式:基于非关系型数据库(非关系型就是非事务性,除此之外也支持关系型数据库),相对于传统事务机制(X/Open XA Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。主要由三阶段完成,Try: 尝试执行业务、 Confirm:确认执行业务、 Cancel: 取消执行业务。特点:完全依赖开发者,通过重写三个方法来实现对事务的控制,基于注解@TccTransaction。

 

 

 

3. TXC模式:命名来源于淘宝,实现原理是在执行SQL之前,先查询SQL的影响数据,然后保存执行的SQL快走信息和创建锁。当需要回滚的时候就采用这些记录数据回滚数据库,目前锁实现依赖redis分布式锁控制。特点:嵌入低,不会占用数据库的连接资源,资源消耗较多,基于注解@TxcTransaction

LCN原理解析

事务发起方和参与方都属于TxClient,TxManager负责控制整个事务。

123代表三个txClient将自己的事务发布到TxManager,4为发起方告诉txManager获取数据成功或者失败,如果成功(其中一个失败也算失败),TxManager返回通知AB让他们各自提交自己的事务。5为txManager通知发起方提交自己的事务。

LCN事务核心步骤

1. 创建事务组:事务发起方在开始执行业务代码之前先调用txManager创建事务组对象,拿到事务表示GroupId。

2. 加入事务组:2和3,参与方将自己的事务执行情况报备给txManager

3. 通知事务组:发起方执行完业务代码之后,将执行结果状态通知给txManager

4. 通知事务单元:txManager询问参与方的事务执行结果

5. 响应通知事务组:txManager通知发起方事务的最终结果

配置Tx-LCN案例

一、新建

1. 执行依赖中带有的tx-manager.sql文件到数据库中

2. 配置application.properties

TC配置

 

# 是否启动LCN负载均衡策略(优化选项,开启与否,功能不受影响)
tx-lcn.ribbon.loadbalancer.dtx.enabled=true

# tx-manager 的配置地址,可以指定TM集群中的任何一个或多个地址
# tx-manager 下集群策略,每个TC都会从始至终<断线重连>与TM集群保持集群大小个连接。
# TM方,每有TM进入集群,会找到所有TC并通知其与新TM建立连接。
# TC方,启动时按配置与集群建立连接,成功后,会再与集群协商,查询集群大小并保持与所有TM的连接
tx-lcn.client.manager-address=127.0.0.1:8070

# 该参数是分布式事务框架存储的业务切面信息。采用的是h2数据库。绝对路径。该参数默认的值为{user.dir}/.txlcn/{application.name}-{application.port}
tx-lcn.aspect.log.file-path=logs/.txlcn/demo-8080

# 调用链长度等级,默认值为3(优化选项。系统中每个请求大致调用链平均长度,估算值。)
tx-lcn.client.chain-level=3

# 该参数为tc与tm通讯时的最大超时时间,单位ms。该参数不需要配置会在连接初始化时由tm返回。
tx-lcn.client.tm-rpc-timeout=2000

# 该参数为分布式事务的最大时间,单位ms。该参数不允许TC方配置,会在连接初始化时由tm返回。
tx-lcn.client.dtx-time=8000

# 该参数为雪花算法的机器编号,所有TC不能相同。该参数不允许配置,会在连接初始化时由tm返回。
tx-lcn.client.machine-id=1

# 该参数为事务方法注解切面的orderNumber,默认值为0.
tx-lcn.client.dtx-aspect-order=0

# 该参数为事务连接资源方法切面的orderNumber,默认值为0.
tx-lcn.client.resource-order=0

# 是否开启日志记录。当开启以后需要配置对应logger的数据库连接配置信息。
tx-lcn.logger.enabled=false
tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
tx-lcn.logger.jdbc-url=${spring.datasource.url} #jdbc:mysql://localhost:3306/tx-manager?characterEncoding=UTF-8  遇上servertimezone问题添加serverTimezone=GMT
tx-lcn.logger.username=${spring.datasource.username} 
tx-lcn.logger.password=${spring.datasource.password}
tx-lcn.logger.enabled=false #如果打开事务:更改 tx-lcn.logger.enabled=true
TM配置
spring.application.name=TransactionManager
server.port=7970

# JDBC 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tx-manager?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456

# 数据库方言
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

# 第一次运行可以设置为: create, 为TM创建持久化数据库表
spring.jpa.hibernate.ddl-auto=validate

# TM监听IP. 默认为 127.0.0.1
tx-lcn.manager.host=127.0.0.1

# TM监听Socket端口. 默认为 ${server.port} - 100
tx-lcn.manager.port=8070

# 心跳检测时间(ms). 默认为 300000
tx-lcn.manager.heart-time=300000

# 分布式事务执行总时间(ms). 默认为36000
tx-lcn.manager.dtx-time=8000

# 参数延迟删除时间单位ms  默认为dtx-time值
tx-lcn.message.netty.attr-delay-time=${tx-lcn.manager.dtx-time}

# 事务处理并发等级. 默认为机器逻辑核心数5倍
tx-lcn.manager.concurrent-level=160

# TM后台登陆密码,默认值为codingapi
tx-lcn.manager.admin-key=codingapi

# 分布式事务锁超时时间 默认为-1,当-1时会用tx-lcn.manager.dtx-time的时间
tx-lcn.manager.dtx-lock-time=${tx-lcn.manager.dtx-time}

# 雪花算法的sequence位长度,默认为12位.
tx-lcn.manager.seq-len=12

# 异常回调开关。开启时请制定ex-url
tx-lcn.manager.ex-url-enabled=false

# 事务异常通知(任何http协议地址。未指定协议时,为TM提供内置功能接口)。默认是邮件通知
tx-lcn.manager.ex-url=/provider/email-to/***@**.com



# 开启日志,默认为false
tx-lcn.logger.enabled=true
tx-lcn.logger.enabled=false
tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
tx-lcn.logger.jdbc-url=${spring.datasource.url}
tx-lcn.logger.username=${spring.datasource.username}
tx-lcn.logger.password=${spring.datasource.password}

# redis 的设置信息. 线上请用Redis Cluster
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=

配置本地事务和分布式事务

@Configuration
@EnableTransactionManagement
public class TransactionConfiguration {

    /**
     * 本地事务配置
     * @param transactionManager
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) {
        Properties properties = new Properties();
        properties.setProperty("*", "PROPAGATION_REQUIRED,-Throwable");
        TransactionInterceptor transactionInterceptor = new TransactionInterceptor();
        transactionInterceptor.setTransactionManager(transactionManager);
        transactionInterceptor.setTransactionAttributes(properties);
        return transactionInterceptor;
    }

    /**
     * 分布式事务配置 设置为LCN模式
     * @param dtxLogicWeaver
     * @return
     */
    @ConditionalOnBean(DTXLogicWeaver.class)
    @Bean
    public TxLcnInterceptor txLcnInterceptor(DTXLogicWeaver dtxLogicWeaver) {
        TxLcnInterceptor txLcnInterceptor = new TxLcnInterceptor(dtxLogicWeaver);
        Properties properties = new Properties();
        properties.setProperty(Transactions.DTX_TYPE,Transactions.LCN);
        properties.setProperty(Transactions.DTX_PROPAGATION, "REQUIRED");
        txLcnInterceptor.setTransactionAttributes(properties);
        return txLcnInterceptor;
    }

    @Bean
    public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
        BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
        //需要调整优先级,分布式事务在前,本地事务在后。
        beanNameAutoProxyCreator.setInterceptorNames("txLcnInterceptor","transactionInterceptor");
        beanNameAutoProxyCreator.setBeanNames("*Impl");
        return beanNameAutoProxyCreator;
    }
}
 

 

二、依赖

TX-Manager配置,独立于微服务,是独立的依赖

<dependencies>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-tm</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Tx-Client依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
 

三、操作案例

 Tx-Student微服务有addStudent()方法,Tc-Teacher微服务有addTeacher()方法,因为student的tid依赖于teacher,所以只有teacher插入成功了s才能插入成功,先不要添加外键。

这时候只在addStudent()方法上@transactional是不够的,@transactional只限于本地事务。而这是分布式事务。

两个方法上都添加

@Transavtional
@TxTransaction

两个工程的启动类上都添加

@EnableDistributedTransaction

在teacher和student的application.properties文件中设置地址

#配置事务管理器TxManager的事务端口
tx-lcn.client.manager-address: 127.0.0.1:8070

代码

@Autowired
private TeacherFeign teacherFeign;
@Autowired
private StudentMapper studentMapper;

 @Transactional
 @TcTransaction
public String addStudent(){
     Student s = new Student();
      teacherFeign.addTeacher(1,"lc老师“);
      s.setId(1);
      s.setName("xx学生");
      s.setTid(1);
     studentMapper.insert(s);
}

访问 http://localhost:7090 可以进入TxManager系统后台,可以看到配置信息及系统日志

配置TCC(非事务型数据库)案例

假设要将Teacher数据存储到mangodb

添加以下依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- 数据源-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--数据源-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.1</version>
</dependency>
<!--连接驱动 -->
<dependency>
    <groupId>mysql</groupId></groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>

 

TCC模式需要开发者手动处理,原因是mongodb没有事务,需要手动处理删除,所以TCC也叫代码补偿业务,其实现需要三个方法基本方法:
  • xxx() 需要加上@TccTransaction注解

  • confirmXxx()

  • cancelXxx()


@Autowired 
private MongoTemplate mongoTemplate;

@TccTransaction
public int addTercher(Teacher teacher){ System.out.println("尝试新增数据") Teacher result = mongoTemplate.insert(teacher); if(result!=null){return 1;} return 0; } public void confirmAddTeacher(Teacher teacher){ System.out.println("teacher="+teacher) //只是有确认的作用 } public void cancelAddTeacher(Teacher teacher){ Criteria criteria = Criteria.where("id").is(teacher.getId()); Query query=new Query(criteria); DeleteResult deleteResut=mongoTemplate.remove(query,Teacher.class); System.out.println("deleteResut.getDeletedCount="+deleteResut.getDeletedCount()) }

student方不需要加@TccTransaction,仍然还是lcn(没有更改数据库)

Seata

组成部分

  • Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,相当于Seata的server
  • Transaction Manager(TM):事务管理器。负责开启一个全局事务,并最终发起全局提交或全局回滚
  • Resource Manger(RM) 资源管理器,向事务管理器注册分支事务和并报告分支事务的状态,负责分支事务提交或回滚

事务控制流程

  1. TM向TC申请开启全局事务(@GlobalTransaction)

  2. TC向TM返回全局事务的唯一XID

  3. 分支服务①向TC申请注册分支服务,TC向分支服务①返回BranchID

  4. 分支服务①进行操作(RM),记录操作日志undo_log,提交本地分支事务,向TC上报事务处理结果

  5. 分支服务①远程调用分支服务②

  6. 分支服务②进行与①一样的操作(RM),得到唯一的BranchID,undo_log以及上报

  7. 分支服务处理完毕后,TM向TC提交全局事务决议,询问是否全部执行成功。

  8. 如果事务出现问题,反向操作undo_log回滚本地事务。

Seata流程

配置案例

一、依赖

<seata.version>1.4.2</seata.version>

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>${seata.version}</version>
</dependency>

https://seata.io/en-us/blog/download.html 下载好对应版本的seata server

1. 修改\seata\conf目录下的file.conf的mode以及db的配置方式,目前mode有三个选项 file、db、redis

file.conf 文件配置

2. 然后在对应数据库创建 globalTable(持久化全局事务)、branchTable(持久化各提交分支的事务)、 lockTable(持久化各分支锁定资源事务)三张表

-- the table to store GlobalSession data
-- 持久化全局事务
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
-- 持久化各提交分支的事务
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
-- 持久化每个分支锁表事务
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;。

3. \seata\conf目录下的registry.conf文件,设置 注册中心 和 配置中心,我选择了都放在nacos(参见《Seata 1.4.0 + nacos配置和使用,超详细》 https://blog.csdn.net/qq853632587/article/details/111644295)

registry.conf 文件配置

4. 第三步下载好的nacos-config.sh放在seata的conf目录下

下载config.txt(https://link.csdn.net/?target=https%3A%2F%2Fgithub.com%2Fseata%2Fseata%2Fblob%2Fdevelop%2Fscript%2Fconfig-center%2Fnacos%2Fnacos-config.sh)seata各种详细的配置,执行 nacos-config.sh 即可将这些配置导入到nacos,这样就不需要将file.conf和registry.conf放到我们的项目中了,需要什么配置就直接从nacos中读取。

config.txt修改

在conf文件git bash

sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 0af6e97b-a684-4647-b696-7c6d42aecce7 -u nacos -w nacos

5. 每个服务都搭建配置bootstrap.yml

spring:
    application:
        name: application_name
    datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/seata
        username: root
        password: 123456

seata:
  enabled: true
  enable-auto-data-source-proxy: true
  tx-service-group: my_test_tx_group
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
      namespace: 自己的namespace
  service:
    vgroup-mapping:
      my_test_tx_group: default
    disable-global-transaction: false
  client:
    rm:
      report-success-enable: false

 在业务逻辑层的方法(不同方法)上方增加@ GlobalTransactional

MQ事务消息

有一些第三方的MQ是支持事务消息的,如RocketMQ。但RabbitMQ和Kafka都不支持。支持事务消息的方式也是类似于二阶段提交。

举例:让支付宝账户扣除一万,我们生成一个凭证(消息),这个消息写着让余额宝账户增加一万。我们可以拿这个消息完成最终一致性。

消息解耦

  1. 支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送(只是知道有这一条消息),只有消息发送成功后才会提交事务

  2. 先把(支付宝-10000)封装成一个消息(new Message())),后把这个消息提交到MQ服务器上,当本地事务操作完成了以后,要么成功:(给MQ一个标识:COMMIT),要么失败:(给MQ一个标识:ROLLBACK)

send(producer.send(new Message(),callback(里面处理本地事务)))
//在callback处理本地事务:在callback方法里:
update A set amount = amount - 10000 where userId = 1;
  1. 当支付宝扣款事务被成功提交后,向实时消息服务确认发送,只有在得到确认发送指令后,实时消息服务才真正发送该消息。

  2. 当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。消息就不会被发送。

  3. 对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态进行更新。

    优点:消息数据独立存储,降低业务系统与消息系统之间的耦合

    缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。

img

 参考:

【分布式事务----LCN】LCN原理及使用方式 https://blog.csdn.net/lgxzzz/article/details/121258819

微服务分布式事务之LCN、TCC特点、事务补偿机制缘由以及设计重点 https://www.cnblogs.com/Courage129/p/14528981.html

分布式事务之TX-LCN  https://cloud.tencent.com/developer/article/1752752

posted @ 2021-11-25 15:23  lc还能code  阅读(2833)  评论(0编辑  收藏  举报