博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
对PostgreSQL xmin的深入学习
阅读量:5788 次
发布时间:2019-06-18

本文共 51737 字,大约阅读时间需要 172 分钟。

当PostgreSQL需要insert 一条记录的时候,它会把记录头放入xmin,xmax等字段。

xmin的值,就是当前的Transaction的TransactionId。这是为了满足MVCC的需要。

跟踪程序进行了解:

/* * Allocate the next XID for a new transaction or subtransaction. * * The new XID is also stored into MyProc before returning. * * Note: when this is called, we are actually already inside a valid * transaction, since XIDs are now not allocated until the transaction * does something.    So it is safe to do a database lookup if we want to * issue a warning about XID wrap. */TransactionIdGetNewTransactionId(bool isSubXact){    TransactionId xid;    /*     * During bootstrap initialization, we return the special bootstrap     * transaction id.     */    if (IsBootstrapProcessingMode())    {        Assert(!isSubXact);        MyProc->xid = BootstrapTransactionId;        return BootstrapTransactionId;    }    /* safety check, we should never get this far in a HS slave */    if (RecoveryInProgress())        elog(ERROR, "cannot assign TransactionIds during recovery");    LWLockAcquire(XidGenLock, LW_EXCLUSIVE);    xid = ShmemVariableCache->nextXid;    //fprintf(stderr,"In GetNewTransactionId--------1, xid is :%d\n",xid);    /*----------     * Check to see if it's safe to assign another XID.  This protects against     * catastrophic data loss due to XID wraparound.  The basic rules are:     *     * If we're past xidVacLimit, start trying to force autovacuum cycles.     * If we're past xidWarnLimit, start issuing warnings.     * If we're past xidStopLimit, refuse to execute transactions, unless     * we are running in a standalone backend (which gives an escape hatch     * to the DBA who somehow got past the earlier defenses).     *----------     */    if (TransactionIdFollowsOrEquals(xid, ShmemVariableCache->xidVacLimit))    {        /*         * For safety's sake, we release XidGenLock while sending signals,         * warnings, etc.  This is not so much because we care about         * preserving concurrency in this situation, as to avoid any         * possibility of deadlock while doing get_database_name(). First,         * copy all the shared values we'll need in this path.         */        TransactionId xidWarnLimit = ShmemVariableCache->xidWarnLimit;        TransactionId xidStopLimit = ShmemVariableCache->xidStopLimit;        TransactionId xidWrapLimit = ShmemVariableCache->xidWrapLimit;        Oid            oldest_datoid = ShmemVariableCache->oldestXidDB;        LWLockRelease(XidGenLock);        /*         * To avoid swamping the postmaster with signals, we issue the autovac         * request only once per 64K transaction starts.  This still gives         * plenty of chances before we get into real trouble.         */        if (IsUnderPostmaster && (xid % 65536) == 0)            SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER);        if (IsUnderPostmaster &&            TransactionIdFollowsOrEquals(xid, xidStopLimit))        {            char       *oldest_datname = get_database_name(oldest_datoid);            /* complain even if that DB has disappeared */            if (oldest_datname)                ereport(ERROR,                        (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),                         errmsg("database is not accepting commands to avoid wraparound data loss in database \"%s\"",                                oldest_datname),                         errhint("Stop the postmaster and use a standalone backend to vacuum that database.\n"                                 "You might also need to commit or roll back old prepared transactions.")));            else                ereport(ERROR,                        (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),                         errmsg("database is not accepting commands to avoid wraparound data loss in database with OID %u",                                oldest_datoid),                         errhint("Stop the postmaster and use a standalone backend to vacuum that database.\n"                                 "You might also need to commit or roll back old prepared transactions.")));        }        else if (TransactionIdFollowsOrEquals(xid, xidWarnLimit))        {            char       *oldest_datname = get_database_name(oldest_datoid);            /* complain even if that DB has disappeared */            if (oldest_datname)                ereport(WARNING,                        (errmsg("database \"%s\" must be vacuumed within %u transactions",                                oldest_datname,                                xidWrapLimit - xid),                         errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.\n"                                 "You might also need to commit or roll back old prepared transactions.")));            else                ereport(WARNING,                        (errmsg("database with OID %u must be vacuumed within %u transactions",                                oldest_datoid,                                xidWrapLimit - xid),                         errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.\n"                                 "You might also need to commit or roll back old prepared transactions.")));        }        /* Re-acquire lock and start over */        LWLockAcquire(XidGenLock, LW_EXCLUSIVE);        xid = ShmemVariableCache->nextXid;    }    /*     * If we are allocating the first XID of a new page of the commit log,     * zero out that commit-log page before returning. We must do this while     * holding XidGenLock, else another xact could acquire and commit a later     * XID before we zero the page.  Fortunately, a page of the commit log     * holds 32K or more transactions, so we don't have to do this very often.     *     * Extend pg_subtrans too.     */    ExtendCLOG(xid);    ExtendSUBTRANS(xid);    /*     * Now advance the nextXid counter.  This must not happen until after we     * have successfully completed ExtendCLOG() --- if that routine fails, we     * want the next incoming transaction to try it again.    We cannot assign     * more XIDs until there is CLOG space for them.     */    TransactionIdAdvance(ShmemVariableCache->nextXid);    /*     * We must store the new XID into the shared ProcArray before releasing     * XidGenLock.    This ensures that every active XID older than     * latestCompletedXid is present in the ProcArray, which is essential for     * correct OldestXmin tracking; see src/backend/access/transam/README.     *     * XXX by storing xid into MyProc without acquiring ProcArrayLock, we are     * relying on fetch/store of an xid to be atomic, else other backends     * might see a partially-set xid here.    But holding both locks at once     * would be a nasty concurrency hit.  So for now, assume atomicity.     *     * Note that readers of PGPROC xid fields should be careful to fetch the     * value only once, rather than assume they can read a value multiple     * times and get the same answer each time.     *     * The same comments apply to the subxact xid count and overflow fields.     *     * A solution to the atomic-store problem would be to give each PGPROC its     * own spinlock used only for fetching/storing that PGPROC's xid and     * related fields.     *     * If there's no room to fit a subtransaction XID into PGPROC, set the     * cache-overflowed flag instead.  This forces readers to look in     * pg_subtrans to map subtransaction XIDs up to top-level XIDs. There is a     * race-condition window, in that the new XID will not appear as running     * until its parent link has been placed into pg_subtrans. However, that     * will happen before anyone could possibly have a reason to inquire about     * the status of the XID, so it seems OK.  (Snapshots taken during this     * window *will* include the parent XID, so they will deliver the correct     * answer later on when someone does have a reason to inquire.)     */    {        /*         * Use volatile pointer to prevent code rearrangement; other backends         * could be examining my subxids info concurrently, and we don't want         * them to see an invalid intermediate state, such as incrementing         * nxids before filling the array entry.  Note we are assuming that         * TransactionId and int fetch/store are atomic.         */        volatile PGPROC *myproc = MyProc;        if (!isSubXact)            myproc->xid = xid;        else        {            int            nxids = myproc->subxids.nxids;            if (nxids < PGPROC_MAX_CACHED_SUBXIDS)            {                myproc->subxids.xids[nxids] = xid;                myproc->subxids.nxids = nxids + 1;            }            else                myproc->subxids.overflowed = true;        }    }    LWLockRelease(XidGenLock);    //fprintf(stderr,"In GetNewTransactionId--------2, xid is :%d\n",xid);    return xid;}
函数 GetNewTransactionId 为 AssignTransactionId 调用:
/* * AssignTransactionId * * Assigns a new permanent XID to the given TransactionState. * We do not assign XIDs to transactions until/unless this is called. * Also, any parent TransactionStates that don't yet have XIDs are assigned * one; this maintains the invariant that a child transaction has an XID * following its parent's. */static voidAssignTransactionId(TransactionState s){    fprintf(stderr,"---------------------In AssignTransactionId\n");    bool        isSubXact = (s->parent != NULL);    ResourceOwner currentOwner;    /* Assert that caller didn't screw up */    Assert(!TransactionIdIsValid(s->transactionId));    Assert(s->state == TRANS_INPROGRESS);    /*     * Ensure parent(s) have XIDs, so that a child always has an XID later     * than its parent.  Musn't recurse here, or we might get a stack overflow     * if we're at the bottom of a huge stack of subtransactions none of which     * have XIDs yet.     */    if (isSubXact && !TransactionIdIsValid(s->parent->transactionId))    {        TransactionState p = s->parent;        TransactionState *parents;        size_t        parentOffset = 0;        parents = palloc(sizeof(TransactionState) * s->nestingLevel);        while (p != NULL && !TransactionIdIsValid(p->transactionId))        {            parents[parentOffset++] = p;            p = p->parent;        }        /*         * This is technically a recursive call, but the recursion will never         * be more than one layer deep.         */        while (parentOffset != 0)            AssignTransactionId(parents[--parentOffset]);        pfree(parents);    }    /*     * Generate a new Xid and record it in PG_PROC and pg_subtrans.     *     * NB: we must make the subtrans entry BEFORE the Xid appears anywhere in     * shared storage other than PG_PROC; because if there's no room for it in     * PG_PROC, the subtrans entry is needed to ensure that other backends see     * the Xid as "running".  See GetNewTransactionId.     */    s->transactionId = GetNewTransactionId(isSubXact);    fprintf(stderr,"In AssignTransactionId transaction is: %d \n",s->transactionId);    if (isSubXact)        SubTransSetParent(s->transactionId, s->parent->transactionId, false);    /*     * If it's a top-level transaction, the predicate locking system needs to     * be told about it too.     */    if (!isSubXact)        RegisterPredicateLockingXid(s->transactionId);    /*     * Acquire lock on the transaction XID.  (We assume this cannot block.) We     * have to ensure that the lock is assigned to the transaction's own     * ResourceOwner.     */    currentOwner = CurrentResourceOwner;    PG_TRY();    {        CurrentResourceOwner = s->curTransactionOwner;        XactLockTableInsert(s->transactionId);    }    PG_CATCH();    {        /* Ensure CurrentResourceOwner is restored on error */        CurrentResourceOwner = currentOwner;        PG_RE_THROW();    }    PG_END_TRY();    CurrentResourceOwner = currentOwner;    /*     * Every PGPROC_MAX_CACHED_SUBXIDS assigned transaction ids within each     * top-level transaction we issue a WAL record for the assignment. We     * include the top-level xid and all the subxids that have not yet been     * reported using XLOG_XACT_ASSIGNMENT records.     *     * This is required to limit the amount of shared memory required in a hot     * standby server to keep track of in-progress XIDs. See notes for     * RecordKnownAssignedTransactionIds().     *     * We don't keep track of the immediate parent of each subxid, only the     * top-level transaction that each subxact belongs to. This is correct in     * recovery only because aborted subtransactions are separately WAL     * logged.     */    if (isSubXact && XLogStandbyInfoActive())    {        unreportedXids[nUnreportedXids] = s->transactionId;        nUnreportedXids++;        /*         * ensure this test matches similar one in         * RecoverPreparedTransactions()         */        if (nUnreportedXids >= PGPROC_MAX_CACHED_SUBXIDS)        {            XLogRecData rdata[2];            xl_xact_assignment xlrec;            /*             * xtop is always set by now because we recurse up transaction             * stack to the highest unassigned xid and then come back down             */            xlrec.xtop = GetTopTransactionId();            Assert(TransactionIdIsValid(xlrec.xtop));            xlrec.nsubxacts = nUnreportedXids;            rdata[0].data = (char *) &xlrec;            rdata[0].len = MinSizeOfXactAssignment;            rdata[0].buffer = InvalidBuffer;            rdata[0].next = &rdata[1];            rdata[1].data = (char *) unreportedXids;            rdata[1].len = PGPROC_MAX_CACHED_SUBXIDS * sizeof(TransactionId);            rdata[1].buffer = InvalidBuffer;            rdata[1].next = NULL;            (void) XLogInsert(RM_XACT_ID, XLOG_XACT_ASSIGNMENT, rdata);            nUnreportedXids = 0;        }    }}

而  AssignTransactionId 函数,为  所调用

/* *    GetCurrentTransactionId * * This will return the XID of the current transaction (main or sub * transaction), assigning one if it's not yet set.  Be careful to call this * only inside a valid xact. */TransactionIdGetCurrentTransactionId(void){    TransactionState s = CurrentTransactionState;    if (!TransactionIdIsValid(s->transactionId))        fprintf(stderr,"transaction id invalid.......\n");    else        fprintf(stderr,"transaction id OK!.......\n"); if (!TransactionIdIsValid(s->transactionId))        AssignTransactionId(s); return s->transactionId;}

而 GetCurrentTransactionId 函数为 heap_insert  函数所调用:

/* *    heap_insert        - insert tuple into a heap * * The new tuple is stamped with current transaction ID and the specified * command ID. * * If the HEAP_INSERT_SKIP_WAL option is specified, the new tuple is not * logged in WAL, even for a non-temp relation.  Safe usage of this behavior * requires that we arrange that all new tuples go into new pages not * containing any tuples from other transactions, and that the relation gets * fsync'd before commit.  (See also heap_sync() comments) * * The HEAP_INSERT_SKIP_FSM option is passed directly to * RelationGetBufferForTuple, which see for more info. * * Note that these options will be applied when inserting into the heap's * TOAST table, too, if the tuple requires any out-of-line data. * * The BulkInsertState object (if any; bistate can be NULL for default * behavior) is also just passed through to RelationGetBufferForTuple. * * The return value is the OID assigned to the tuple (either here or by the * caller), or InvalidOid if no OID.  The header fields of *tup are updated * to match the stored tuple; in particular tup->t_self receives the actual * TID where the tuple was stored.    But note that any toasting of fields * within the tuple data is NOT reflected into *tup. */Oidheap_insert(Relation relation, HeapTuple tup, CommandId cid,            int options, BulkInsertState bistate){    fprintf(stderr,"In heap_insert------------------------------1\n");    TransactionId xid = GetCurrentTransactionId();    fprintf(stderr,"xid is :%d.......\n",(int)xid);    HeapTuple    heaptup;    Buffer        buffer;    bool        all_visible_cleared = false;    if (relation->rd_rel->relhasoids)    {#ifdef NOT_USED        /* this is redundant with an Assert in HeapTupleSetOid */        Assert(tup->t_data->t_infomask & HEAP_HASOID);#endif        /*         * If the object id of this tuple has already been assigned, trust the         * caller.    There are a couple of ways this can happen.  At initial db         * creation, the backend program sets oids for tuples. When we define         * an index, we set the oid.  Finally, in the future, we may allow         * users to set their own object ids in order to support a persistent         * object store (objects need to contain pointers to one another).         */        if (!OidIsValid(HeapTupleGetOid(tup)))            HeapTupleSetOid(tup, GetNewOid(relation));    }    else    {        /* check there is not space for an OID */        Assert(!(tup->t_data->t_infomask & HEAP_HASOID));    }    tup->t_data->t_infomask &= ~(HEAP_XACT_MASK);    tup->t_data->t_infomask2 &= ~(HEAP2_XACT_MASK);    tup->t_data->t_infomask |= HEAP_XMAX_INVALID;    HeapTupleHeaderSetXmin(tup->t_data, xid);    HeapTupleHeaderSetCmin(tup->t_data, cid);    HeapTupleHeaderSetXmax(tup->t_data, 0);        /* for cleanliness */    tup->t_tableOid = RelationGetRelid(relation);    /*     * If the new tuple is too big for storage or contains already toasted     * out-of-line attributes from some other relation, invoke the toaster.     *     * Note: below this point, heaptup is the data we actually intend to store     * into the relation; tup is the caller's original untoasted data.     */    if (relation->rd_rel->relkind != RELKIND_RELATION)    {        /* toast table entries should never be recursively toasted */        Assert(!HeapTupleHasExternal(tup));        heaptup = tup;    }    else if (HeapTupleHasExternal(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD)        heaptup = toast_insert_or_update(relation, tup, NULL, options);    else        heaptup = tup;    /*     * We're about to do the actual insert -- but check for conflict first,     * to avoid possibly having to roll back work we've just done.     *     * For a heap insert, we only need to check for table-level SSI locks.     * Our new tuple can't possibly conflict with existing tuple locks, and     * heap page locks are only consolidated versions of tuple locks; they do     * not lock "gaps" as index page locks do.  So we don't need to identify     * a buffer before making the call.     */    CheckForSerializableConflictIn(relation, NULL, InvalidBuffer);    /* Find buffer to insert this tuple into */    buffer = RelationGetBufferForTuple(relation, heaptup->t_len,                                       InvalidBuffer, options, bistate);    /* NO EREPORT(ERROR) from here till changes are logged */    START_CRIT_SECTION();    RelationPutHeapTuple(relation, buffer, heaptup);    if (PageIsAllVisible(BufferGetPage(buffer)))    {        all_visible_cleared = true;        PageClearAllVisible(BufferGetPage(buffer));    }    /*     * XXX Should we set PageSetPrunable on this page ?     *     * The inserting transaction may eventually abort thus making this tuple     * DEAD and hence available for pruning. Though we don't want to optimize     * for aborts, if no other tuple in this page is UPDATEd/DELETEd, the     * aborted tuple will never be pruned until next vacuum is triggered.     *     * If you do add PageSetPrunable here, add it in heap_xlog_insert too.     */    MarkBufferDirty(buffer);    /* XLOG stuff */    if (!(options & HEAP_INSERT_SKIP_WAL) && RelationNeedsWAL(relation))    {        xl_heap_insert xlrec;        xl_heap_header xlhdr;        XLogRecPtr    recptr;        XLogRecData rdata[3];        Page        page = BufferGetPage(buffer);        uint8        info = XLOG_HEAP_INSERT;        xlrec.all_visible_cleared = all_visible_cleared;        xlrec.target.node = relation->rd_node;        xlrec.target.tid = heaptup->t_self;        rdata[0].data = (char *) &xlrec;        rdata[0].len = SizeOfHeapInsert;        rdata[0].buffer = InvalidBuffer;        rdata[0].next = &(rdata[1]);        xlhdr.t_infomask2 = heaptup->t_data->t_infomask2;        xlhdr.t_infomask = heaptup->t_data->t_infomask;        xlhdr.t_hoff = heaptup->t_data->t_hoff;        /*         * note we mark rdata[1] as belonging to buffer; if XLogInsert decides         * to write the whole page to the xlog, we don't need to store         * xl_heap_header in the xlog.         */        rdata[1].data = (char *) &xlhdr;        rdata[1].len = SizeOfHeapHeader;        rdata[1].buffer = buffer;        rdata[1].buffer_std = true;        rdata[1].next = &(rdata[2]);        /* PG73FORMAT: write bitmap [+ padding] [+ oid] + data */        rdata[2].data = (char *) heaptup->t_data + offsetof(HeapTupleHeaderData, t_bits);        rdata[2].len = heaptup->t_len - offsetof(HeapTupleHeaderData, t_bits);        rdata[2].buffer = buffer;        rdata[2].buffer_std = true;        rdata[2].next = NULL;        /*         * If this is the single and first tuple on page, we can reinit the         * page instead of restoring the whole thing.  Set flag, and hide         * buffer references from XLogInsert.         */        if (ItemPointerGetOffsetNumber(&(heaptup->t_self)) == FirstOffsetNumber &&            PageGetMaxOffsetNumber(page) == FirstOffsetNumber)        {            info |= XLOG_HEAP_INIT_PAGE;            rdata[1].buffer = rdata[2].buffer = InvalidBuffer;        }        recptr = XLogInsert(RM_HEAP_ID, info, rdata);        PageSetLSN(page, recptr);        PageSetTLI(page, ThisTimeLineID);    }    END_CRIT_SECTION();    UnlockReleaseBuffer(buffer);    /* Clear the bit in the visibility map if necessary */    if (all_visible_cleared)        visibilitymap_clear(relation,                            ItemPointerGetBlockNumber(&(heaptup->t_self)));    /*     * If tuple is cachable, mark it for invalidation from the caches in case     * we abort.  Note it is OK to do this after releasing the buffer, because     * the heaptup data structure is all in local memory, not in the shared     * buffer.     */    CacheInvalidateHeapTuple(relation, heaptup);    pgstat_count_heap_insert(relation);    /*     * If heaptup is a private copy, release it.  Don't forget to copy t_self     * back to the caller's image, too.     */    if (heaptup != tup)    {        tup->t_self = heaptup->t_self;        heap_freetuple(heaptup);    }    fprintf(stderr,"In heap_insert------------------------------2\n");    return HeapTupleGetOid(tup);}

而heap_insert 函数为   所调用:

/* ---------------------------------------------------------------- *        ExecInsert * *        For INSERT, we have to insert the tuple into the target relation *        and insert appropriate tuples into the index relations. * *        Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */static TupleTableSlot *ExecInsert(TupleTableSlot *slot,           TupleTableSlot *planSlot,           EState *estate,           bool canSetTag){    HeapTuple    tuple;    ResultRelInfo *resultRelInfo;    Relation    resultRelationDesc;    Oid            newId;    List       *recheckIndexes = NIL;    /**    if (slot == NULL)        fprintf(stderr,"---In ExecInsert...slot is null\n");    else        fprintf(stderr,"---In ExecInsert...slot is not null\n");    fprintf(stderr,"IN ExecInsert-----------------------------100\n");    if (slot->tts_isempty)        fprintf(stderr,"slot. tts_isempty!\n");    else    {        fprintf(stderr,"slot, tts not empty!\n");        HeapTuple htp = slot->tts_tuple;        if (htp == NULL)            fprintf(stderr,"htp is NULL\n");        else            fprintf(stderr,"htp is NOT NULL\n");    }    fprintf(stderr,"--------------------------------------------\n");    */    //    /*     * get the heap tuple out of the tuple table slot, making sure we have a     * writable copy     */    tuple = ExecMaterializeSlot(slot);    fprintf(stderr,"--------------------------------------------150\n");    if (slot->tts_isempty)        fprintf(stderr,"slot. tts_isempty!\n");    else    {        ///fprintf(stderr,"slot, tts not empty!\n");        HeapTuple htp = slot->tts_tuple;        HeapTupleHeader theader = htp->t_data;        if (theader == NULL)            fprintf(stderr,"heap tuple header is NULL\n");        else{            ////fprintf(stderr,"heap tuple header is NOT NULL\n");            HeapTupleFields htfds = theader->t_choice.t_heap;            TransactionId txmin = htfds.t_xmin;            TransactionId txmax = htfds.t_xmax;            fprintf(stderr,"t_xmin is :%d ", (int)txmin );            fprintf(stderr,"t_xmax is :%d \n", (int)txmax );        }    }    /*     * get information on the (current) result relation     */    resultRelInfo = estate->es_result_relation_info;    resultRelationDesc = resultRelInfo->ri_RelationDesc;    /*     * If the result relation has OIDs, force the tuple's OID to zero so that     * heap_insert will assign a fresh OID.  Usually the OID already will be     * zero at this point, but there are corner cases where the plan tree can     * return a tuple extracted literally from some table with the same     * rowtype.     *     * XXX if we ever wanted to allow users to assign their own OIDs to new     * rows, this'd be the place to do it.  For the moment, we make a point of     * doing this before calling triggers, so that a user-supplied trigger     * could hack the OID if desired.     */    if (resultRelationDesc->rd_rel->relhasoids)        HeapTupleSetOid(tuple, InvalidOid);    /* BEFORE ROW INSERT Triggers */    if (resultRelInfo->ri_TrigDesc &&        resultRelInfo->ri_TrigDesc->trig_insert_before_row)    {        slot = ExecBRInsertTriggers(estate, resultRelInfo, slot);        if (slot == NULL)        /* "do nothing" */            return NULL;        /* trigger might have changed tuple */        tuple = ExecMaterializeSlot(slot);    }    fprintf(stderr,"--------------------------------------------200\n");    if (slot->tts_isempty)        fprintf(stderr,"slot. tts_isempty!\n");    else    {        ///fprintf(stderr,"slot, tts not empty!\n");        HeapTuple htp = slot->tts_tuple;        HeapTupleHeader theader = htp->t_data;        if (theader == NULL)            fprintf(stderr,"heap tuple header is NULL\n");        else{            ////fprintf(stderr,"heap tuple header is NOT NULL\n");            HeapTupleFields htfds = theader->t_choice.t_heap;            TransactionId txmin = htfds.t_xmin;            TransactionId txmax = htfds.t_xmax;            fprintf(stderr,"t_xmin is :%d ", (int)txmin );            fprintf(stderr,"t_xmax is :%d \n", (int)txmax );        }    }    /* INSTEAD OF ROW INSERT Triggers */    if (resultRelInfo->ri_TrigDesc &&        resultRelInfo->ri_TrigDesc->trig_insert_instead_row)    {        ////fprintf(stderr,"x--------------------------1\n");        slot = ExecIRInsertTriggers(estate, resultRelInfo, slot);        if (slot == NULL)        /* "do nothing" */            return NULL;        /* trigger might have changed tuple */        tuple = ExecMaterializeSlot(slot);        newId = InvalidOid;    }    else    {        fprintf(stderr,"x--------------------------2\n");        /*         * Check the constraints of the tuple         */        if (resultRelationDesc->rd_att->constr)            ExecConstraints(resultRelInfo, slot, estate);        fprintf(stderr,"IN ExecInsert-----------------------------210\n");        if (slot->tts_isempty)            fprintf(stderr,"slot. tts_isempty!\n");        else        {            ///fprintf(stderr,"slot, tts not empty!\n");            HeapTuple htp = slot->tts_tuple;            HeapTupleHeader theader = htp->t_data;            if (theader == NULL)                fprintf(stderr,"heap tuple header is NULL\n");            else{               ///fprintf(stderr,"heap tuple header is NOT NULL\n");                HeapTupleFields htfds = theader->t_choice.t_heap;                TransactionId txmin = htfds.t_xmin;                TransactionId txmax = htfds.t_xmax;                fprintf(stderr,"t_xmin is :%d ", (int)txmin );                fprintf(stderr,"t_xmax is :%d \n", (int)txmax );                /**                if ( htfds == NULL )                    fprintf(stderr,"t_heap is NULL\n");                else                    fprintf(stderr,"t_heap is not NULL\n");                */            }        }        /*         * insert the tuple         *         * Note: heap_insert returns the tid (location) of the new tuple in         * the t_self field.         */        newId = heap_insert(resultRelationDesc, tuple,                            estate->es_output_cid, 0, NULL);        fprintf(stderr,"IN ExecInsert-----------------------------230\n");        if (slot->tts_isempty)            fprintf(stderr,"slot. tts_isempty!\n");        else        {            ///fprintf(stderr,"slot, tts not empty!\n");            HeapTuple htp = slot->tts_tuple;            HeapTupleHeader theader = htp->t_data;            if (theader == NULL)                fprintf(stderr,"heap tuple header is NULL\n");            else{                ///fprintf(stderr,"heap tuple header is NOT NULL\n");                HeapTupleFields htfds = theader->t_choice.t_heap;                TransactionId txmin = htfds.t_xmin;                TransactionId txmax = htfds.t_xmax;                fprintf(stderr,"t_xmin is :%d ", (int)txmin );                fprintf(stderr,"t_xmax is :%d \n", (int)txmax );                /**                if ( htfds == NULL )                    fprintf(stderr,"t_heap is NULL\n");                else                    fprintf(stderr,"t_heap is not NULL\n");                */            }        }        /*         * insert index entries for tuple         */        if (resultRelInfo->ri_NumIndices > 0)            recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),                                                   estate);    }    fprintf(stderr,"IN ExecInsert-----------------------------250\n");    if (slot->tts_isempty)        fprintf(stderr,"slot. tts_isempty!\n");    else    {        ///fprintf(stderr,"slot, tts not empty!\n");        HeapTuple htp = slot->tts_tuple;        HeapTupleHeader theader = htp->t_data;        if (theader == NULL)            fprintf(stderr,"heap tuple header is NULL\n");        else{            ///fprintf(stderr,"heap tuple header is NOT NULL\n");            HeapTupleFields htfds = theader->t_choice.t_heap;            TransactionId txmin = htfds.t_xmin;            TransactionId txmax = htfds.t_xmax;            fprintf(stderr,"t_xmin is :%d ", (int)txmin );            fprintf(stderr,"t_xmax is :%d \n", (int)txmax );            /**            if ( htfds == NULL )                fprintf(stderr,"t_heap is NULL\n");            else                fprintf(stderr,"t_heap is not NULL\n");            */        }    }    if (canSetTag)    {        (estate->es_processed)++;        estate->es_lastoid = newId;        setLastTid(&(tuple->t_self));    }    /* AFTER ROW INSERT Triggers */    ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);    fprintf(stderr,"IN ExecInsert-----------------------------300\n");    if (slot->tts_isempty)        fprintf(stderr,"slot. tts_isempty!\n");    else    {        ///fprintf(stderr,"slot, tts not empty!\n");        HeapTuple htp = slot->tts_tuple;        HeapTupleHeader theader = htp->t_data;        if (theader == NULL)            fprintf(stderr,"heap tuple header is NULL\n");        else{            ///fprintf(stderr,"heap tuple header is NOT NULL\n");            HeapTupleFields htfds = theader->t_choice.t_heap;            TransactionId txmin = htfds.t_xmin;            TransactionId txmax = htfds.t_xmax;            fprintf(stderr,"t_xmin is :%d ", (int)txmin );            fprintf(stderr,"t_xmax is :%d \n", (int)txmax );            /**            if ( htfds == NULL )                fprintf(stderr,"t_heap is NULL\n");            else                fprintf(stderr,"t_heap is not NULL\n");            */        }    }    list_free(recheckIndexes);    /* Process RETURNING if present */    if (resultRelInfo->ri_projectReturning)        return ExecProcessReturning(resultRelInfo->ri_projectReturning,                                    slot, planSlot);    return NULL;}

 接着要看这个:

typedef struct VariableCacheData{    /*     * These fields are protected by OidGenLock.     */    Oid            nextOid;        /* next OID to assign */    uint32        oidCount;        /* OIDs available before must do XLOG work */    /*     * These fields are protected by XidGenLock.     */    TransactionId nextXid;        /* next XID to assign */    TransactionId oldestXid;    /* cluster-wide minimum datfrozenxid */    TransactionId xidVacLimit;    /* start forcing autovacuums here */    TransactionId xidWarnLimit; /* start complaining here */    TransactionId xidStopLimit; /* refuse to advance nextXid beyond here */    TransactionId xidWrapLimit; /* where the world ends */    Oid            oldestXidDB;    /* database with minimum datfrozenxid */    /*     * These fields are protected by ProcArrayLock.     */    TransactionId latestCompletedXid;    /* newest XID that has committed or                                         * aborted */} VariableCacheData;typedef VariableCacheData *VariableCache;

 现在已经知道,xmin是从 ShmemVairableCache->nextXid得来。

而且,即使数据库重新启动后,xmin也能在上一次的基础上继续增加。

可以知道,对其应当是进行了初始化。

而xmin的来源是 checkPoint数据,它是从数据库文件中来。

目前所知:调用关系

PostmasterMain-->StartupDataBase 

StartupDataBase是一个宏,展开后是 : StartChildProcess(StartupProcess)

StartupProcess-->StartupXLOG

在StartupXLOG中,有如下的代码:

/*         * Get the last valid checkpoint record.  If the latest one according         * to pg_control is broken, try the next-to-last one.         */        checkPointLoc = ControlFile->checkPoint;        RedoStartLSN = ControlFile->checkPointCopy.redo;        record = ReadCheckpointRecord(checkPointLoc, 1);
ShmemVariableCache->nextXid = checkPoint.nextXid;    ShmemVariableCache->nextOid = checkPoint.nextOid;

 为何说 ShmemVariableCache是在共享内存中呢,下面代码会有所启示:

/* *    InitShmemAllocation() --- set up shared-memory space allocation. * * This should be called only in the postmaster or a standalone backend. */voidInitShmemAllocation(void){    fprintf(stderr,"In InitShmemAllocation.....start by process %d\n",getpid());    PGShmemHeader *shmhdr = ShmemSegHdr;    Assert(shmhdr != NULL);    /*     * Initialize the spinlock used by ShmemAlloc.    We have to do the space     * allocation the hard way, since obviously ShmemAlloc can't be called     * yet.     */    ShmemLock = (slock_t *) (((char *) shmhdr) + shmhdr->freeoffset);    shmhdr->freeoffset += MAXALIGN(sizeof(slock_t));    Assert(shmhdr->freeoffset <= shmhdr->totalsize);    SpinLockInit(ShmemLock);    /* ShmemIndex can't be set up yet (need LWLocks first) */    shmhdr->index = NULL;    ShmemIndex = (HTAB *) NULL;    /*     * Initialize ShmemVariableCache for transaction manager. (This doesn't     * really belong here, but not worth moving.)     */    ShmemVariableCache = (VariableCache)        ShmemAlloc(sizeof(*ShmemVariableCache));    memset(ShmemVariableCache, 0, sizeof(*ShmemVariableCache));    fprintf(stderr,"When Initiating, nextXid is: %d \n", (int)ShmemVariableCache->nextXid);    fprintf(stderr,"In InitShmemAllocation.....end by process %d\n\n",getpid());}

而ShmemAlloc是关键:

/* * ShmemAlloc -- allocate max-aligned chunk from shared memory * * Assumes ShmemLock and ShmemSegHdr are initialized. * * Returns: real pointer to memory or NULL if we are out *        of space.  Has to return a real pointer in order *        to be compatible with malloc(). */void *ShmemAlloc(Size size){    Size        newStart;    Size        newFree;    void       *newSpace;    /* use volatile pointer to prevent code rearrangement */    volatile PGShmemHeader *shmemseghdr = ShmemSegHdr;    /*     * ensure all space is adequately aligned.     */    size = MAXALIGN(size);    Assert(shmemseghdr != NULL);    SpinLockAcquire(ShmemLock);    newStart = shmemseghdr->freeoffset;    /* extra alignment for large requests, since they are probably buffers */    if (size >= BLCKSZ)        newStart = BUFFERALIGN(newStart);    newFree = newStart + size;    if (newFree <= shmemseghdr->totalsize)    {        newSpace = (void *) ((char *) ShmemBase + newStart);        shmemseghdr->freeoffset = newFree;    }    else        newSpace = NULL;    SpinLockRelease(ShmemLock);    if (!newSpace)        ereport(WARNING,                (errcode(ERRCODE_OUT_OF_MEMORY),                 errmsg("out of shared memory")));    return newSpace;}
/* shared memory global variables */static PGShmemHeader *ShmemSegHdr;        /* shared mem segment header */static void *ShmemBase;            /* start address of shared memory */static void *ShmemEnd;            /* end+1 address of shared memory */slock_t    *ShmemLock;            /* spinlock for shared memory and LWLock                                 * allocation */static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem *//* *    InitShmemAccess() --- set up basic pointers to shared memory. * * Note: the argument should be declared "PGShmemHeader *seghdr", * but we use void to avoid having to include ipc.h in shmem.h. */voidInitShmemAccess(void *seghdr){    PGShmemHeader *shmhdr = (PGShmemHeader *) seghdr;    ShmemSegHdr = shmhdr;    ShmemBase = (void *) shmhdr;    ShmemEnd = (char *) ShmemBase + shmhdr->totalsize;}

 数据库系统启动的时候的情形已经有所了解了。那么运行中,transaction id 是如何递增的呢。如果我运行两次 GetNewTransactionId,就可以发现 transactionid 每次加2了。

/* * AssignTransactionId * * Assigns a new permanent XID to the given TransactionState. * We do not assign XIDs to transactions until/unless this is called. * Also, any parent TransactionStates that don't yet have XIDs are assigned * one; this maintains the invariant that a child transaction has an XID * following its parent's. */static voidAssignTransactionId(TransactionState s){    fprintf(stderr,"************---------------------In AssignTransactionId..start by process %d\n",getpid());    bool        isSubXact = (s->parent != NULL);    ResourceOwner currentOwner;    /* Assert that caller didn't screw up */    Assert(!TransactionIdIsValid(s->transactionId));    Assert(s->state == TRANS_INPROGRESS);    /*     * Ensure parent(s) have XIDs, so that a child always has an XID later     * than its parent.  Musn't recurse here, or we might get a stack overflow     * if we're at the bottom of a huge stack of subtransactions none of which     * have XIDs yet.     */    if (isSubXact && !TransactionIdIsValid(s->parent->transactionId))    {        TransactionState p = s->parent;        TransactionState *parents;        size_t        parentOffset = 0;        parents = palloc(sizeof(TransactionState) * s->nestingLevel);        while (p != NULL && !TransactionIdIsValid(p->transactionId))        {            parents[parentOffset++] = p;            p = p->parent;        }        /*         * This is technically a recursive call, but the recursion will never         * be more than one layer deep.         */        while (parentOffset != 0)            AssignTransactionId(parents[--parentOffset]);        pfree(parents);    }    /*     * Generate a new Xid and record it in PG_PROC and pg_subtrans.     *     * NB: we must make the subtrans entry BEFORE the Xid appears anywhere in     * shared storage other than PG_PROC; because if there's no room for it in     * PG_PROC, the subtrans entry is needed to ensure that other backends see     * the Xid as "running".  See GetNewTransactionId.     */    s->transactionId = GetNewTransactionId(isSubXact); #####added by gaojian    fprintf(stderr,"In AssignTransactionId ....1.... transaction is: %d \n",s->transactionId);    s->transactionId = GetNewTransactionId(isSubXact);    fprintf(stderr,"In AssignTransactionId ....2.... transaction is: %d \n",s->transactionId);    /////#####added by gaojian    if (isSubXact)        SubTransSetParent(s->transactionId, s->parent->transactionId, false);    /*     * If it's a top-level transaction, the predicate locking system needs to     * be told about it too.     */    if (!isSubXact)        RegisterPredicateLockingXid(s->transactionId);    /*     * Acquire lock on the transaction XID.  (We assume this cannot block.) We     * have to ensure that the lock is assigned to the transaction's own     * ResourceOwner.     */    currentOwner = CurrentResourceOwner;    PG_TRY();    {        CurrentResourceOwner = s->curTransactionOwner;        XactLockTableInsert(s->transactionId);    }    PG_CATCH();    {        /* Ensure CurrentResourceOwner is restored on error */        CurrentResourceOwner = currentOwner;        PG_RE_THROW();    }    PG_END_TRY();    CurrentResourceOwner = currentOwner;    /*     * Every PGPROC_MAX_CACHED_SUBXIDS assigned transaction ids within each     * top-level transaction we issue a WAL record for the assignment. We     * include the top-level xid and all the subxids that have not yet been     * reported using XLOG_XACT_ASSIGNMENT records.     *     * This is required to limit the amount of shared memory required in a hot     * standby server to keep track of in-progress XIDs. See notes for     * RecordKnownAssignedTransactionIds().     *     * We don't keep track of the immediate parent of each subxid, only the     * top-level transaction that each subxact belongs to. This is correct in     * recovery only because aborted subtransactions are separately WAL     * logged.     */    if (isSubXact && XLogStandbyInfoActive())    {        unreportedXids[nUnreportedXids] = s->transactionId;        nUnreportedXids++;        /*         * ensure this test matches similar one in         * RecoverPreparedTransactions()         */        if (nUnreportedXids >= PGPROC_MAX_CACHED_SUBXIDS)        {            XLogRecData rdata[2];            xl_xact_assignment xlrec;            /*             * xtop is always set by now because we recurse up transaction             * stack to the highest unassigned xid and then come back down             */            xlrec.xtop = GetTopTransactionId();            Assert(TransactionIdIsValid(xlrec.xtop));            xlrec.nsubxacts = nUnreportedXids;            rdata[0].data = (char *) &xlrec;            rdata[0].len = MinSizeOfXactAssignment;            rdata[0].buffer = InvalidBuffer;            rdata[0].next = &rdata[1];            rdata[1].data = (char *) unreportedXids;            rdata[1].len = PGPROC_MAX_CACHED_SUBXIDS * sizeof(TransactionId);            rdata[1].buffer = InvalidBuffer;            rdata[1].next = NULL;            (void) XLogInsert(RM_XACT_ID, XLOG_XACT_ASSIGNMENT, rdata);            nUnreportedXids = 0;        }    }    fprintf(stderr,"---------------------In AssignTransactionId end..by process %d\n\n",getpid());}

关键在这个:GetNewTransactionId函数中,调用了 TransactionIdAdvance(ShmemVariableCache->nextXid)

/* * Allocate the next XID for a new transaction or subtransaction. * * The new XID is also stored into MyProc before returning. * * Note: when this is called, we are actually already inside a valid * transaction, since XIDs are now not allocated until the transaction * does something.    So it is safe to do a database lookup if we want to * issue a warning about XID wrap. */TransactionIdGetNewTransactionId(bool isSubXact){    fprintf(stderr,"*********In GetNewTransactionId.....start by process %d\n",getpid());    TransactionId xid;    /*     * During bootstrap initialization, we return the special bootstrap     * transaction id.     */    if (IsBootstrapProcessingMode())    {        Assert(!isSubXact);        MyProc->xid = BootstrapTransactionId;        return BootstrapTransactionId;    }    /* safety check, we should never get this far in a HS slave */    if (RecoveryInProgress())        elog(ERROR, "cannot assign TransactionIds during recovery");    LWLockAcquire(XidGenLock, LW_EXCLUSIVE);    xid = ShmemVariableCache->nextXid;    fprintf(stderr,"In GetNewTransactionId--------1, xid is :%d\n",xid);    /*----------     * Check to see if it's safe to assign another XID.  This protects against     * catastrophic data loss due to XID wraparound.  The basic rules are:     *     * If we're past xidVacLimit, start trying to force autovacuum cycles.     * If we're past xidWarnLimit, start issuing warnings.     * If we're past xidStopLimit, refuse to execute transactions, unless     * we are running in a standalone backend (which gives an escape hatch     * to the DBA who somehow got past the earlier defenses).     *----------     */    if (TransactionIdFollowsOrEquals(xid, ShmemVariableCache->xidVacLimit))    {        /*         * For safety's sake, we release XidGenLock while sending signals,         * warnings, etc.  This is not so much because we care about         * preserving concurrency in this situation, as to avoid any         * possibility of deadlock while doing get_database_name(). First,         * copy all the shared values we'll need in this path.         */        TransactionId xidWarnLimit = ShmemVariableCache->xidWarnLimit;        TransactionId xidStopLimit = ShmemVariableCache->xidStopLimit;        TransactionId xidWrapLimit = ShmemVariableCache->xidWrapLimit;        Oid            oldest_datoid = ShmemVariableCache->oldestXidDB;        LWLockRelease(XidGenLock);        /*         * To avoid swamping the postmaster with signals, we issue the autovac         * request only once per 64K transaction starts.  This still gives         * plenty of chances before we get into real trouble.         */        if (IsUnderPostmaster && (xid % 65536) == 0)            SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER);        if (IsUnderPostmaster &&            TransactionIdFollowsOrEquals(xid, xidStopLimit))        {            char       *oldest_datname = get_database_name(oldest_datoid);            /* complain even if that DB has disappeared */            if (oldest_datname)                ereport(ERROR,                        (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),                         errmsg("database is not accepting commands to avoid wraparound data loss in database \"%s\"",                                oldest_datname),                         errhint("Stop the postmaster and use a standalone backend to vacuum that database.\n"                                 "You might also need to commit or roll back old prepared transactions.")));            else                ereport(ERROR,                        (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),                         errmsg("database is not accepting commands to avoid wraparound data loss in database with OID %u",                                oldest_datoid),                         errhint("Stop the postmaster and use a standalone backend to vacuum that database.\n"                                 "You might also need to commit or roll back old prepared transactions.")));        }        else if (TransactionIdFollowsOrEquals(xid, xidWarnLimit))        {            char       *oldest_datname = get_database_name(oldest_datoid);            /* complain even if that DB has disappeared */            if (oldest_datname)                ereport(WARNING,                        (errmsg("database \"%s\" must be vacuumed within %u transactions",                                oldest_datname,                                xidWrapLimit - xid),                         errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.\n"                                 "You might also need to commit or roll back old prepared transactions.")));            else                ereport(WARNING,                        (errmsg("database with OID %u must be vacuumed within %u transactions",                                oldest_datoid,                                xidWrapLimit - xid),                         errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.\n"                                 "You might also need to commit or roll back old prepared transactions.")));        }        /* Re-acquire lock and start over */        LWLockAcquire(XidGenLock, LW_EXCLUSIVE);        xid = ShmemVariableCache->nextXid;    }    /*     * If we are allocating the first XID of a new page of the commit log,     * zero out that commit-log page before returning. We must do this while     * holding XidGenLock, else another xact could acquire and commit a later     * XID before we zero the page.  Fortunately, a page of the commit log     * holds 32K or more transactions, so we don't have to do this very often.     *     * Extend pg_subtrans too.     */    ExtendCLOG(xid);    ExtendSUBTRANS(xid);    /*     * Now advance the nextXid counter.  This must not happen until after we     * have successfully completed ExtendCLOG() --- if that routine fails, we     * want the next incoming transaction to try it again.    We cannot assign     * more XIDs until there is CLOG space for them.     */    TransactionIdAdvance(ShmemVariableCache->nextXid);   /*     * We must store the new XID into the shared ProcArray before releasing     * XidGenLock.    This ensures that every active XID older than     * latestCompletedXid is present in the ProcArray, which is essential for     * correct OldestXmin tracking; see src/backend/access/transam/README.     *     * XXX by storing xid into MyProc without acquiring ProcArrayLock, we are     * relying on fetch/store of an xid to be atomic, else other backends     * might see a partially-set xid here.    But holding both locks at once     * would be a nasty concurrency hit.  So for now, assume atomicity.     *     * Note that readers of PGPROC xid fields should be careful to fetch the     * value only once, rather than assume they can read a value multiple     * times and get the same answer each time.     *     * The same comments apply to the subxact xid count and overflow fields.     *     * A solution to the atomic-store problem would be to give each PGPROC its     * own spinlock used only for fetching/storing that PGPROC's xid and     * related fields.     *     * If there's no room to fit a subtransaction XID into PGPROC, set the     * cache-overflowed flag instead.  This forces readers to look in     * pg_subtrans to map subtransaction XIDs up to top-level XIDs. There is a     * race-condition window, in that the new XID will not appear as running     * until its parent link has been placed into pg_subtrans. However, that     * will happen before anyone could possibly have a reason to inquire about     * the status of the XID, so it seems OK.  (Snapshots taken during this     * window *will* include the parent XID, so they will deliver the correct     * answer later on when someone does have a reason to inquire.)     */    {        /*         * Use volatile pointer to prevent code rearrangement; other backends         * could be examining my subxids info concurrently, and we don't want         * them to see an invalid intermediate state, such as incrementing         * nxids before filling the array entry.  Note we are assuming that         * TransactionId and int fetch/store are atomic.         */        volatile PGPROC *myproc = MyProc;        if (!isSubXact)            myproc->xid = xid;        else        {            int            nxids = myproc->subxids.nxids;            if (nxids < PGPROC_MAX_CACHED_SUBXIDS)            {                myproc->subxids.xids[nxids] = xid;                myproc->subxids.nxids = nxids + 1;            }            else                myproc->subxids.overflowed = true;        }    }    LWLockRelease(XidGenLock);    fprintf(stderr,"****************In GetNewTransactionId...xid is:%d..end by process %d\n\n",xid,getpid());    return xid;}

转载地址:http://exqyx.baihongyu.com/

你可能感兴趣的文章
PC-IIS因为端口问题报错的解决方法
查看>>
java四种线程池简介,使用
查看>>
ios View之间的切换 屏幕旋转
查看>>
typedef BOOL(WINAPI *MYFUNC) (HWND,COLORREF,BYTE,DWORD);语句的理解
查看>>
jsp 特殊标签
查看>>
[BZOJ] 1012 [JSOI2008]最大数maxnumber
查看>>
gauss消元
查看>>
多线程-ReentrantLock
查看>>
数据结构之链表与哈希表
查看>>
IIS7/8下提示 HTTP 错误 404.13 - Not Found 请求筛选模块被配置为拒绝超过请求内容长度的请求...
查看>>
http返回状态码含义
查看>>
响应式网站对百度友好关键
查看>>
洛谷P2179 骑行川藏
查看>>
(十八)js控制台方法
查看>>
VB关键字总结
查看>>
android代码生成jar包并混淆
查看>>
一个不错的vue项目
查看>>
屏蔽指定IP访问网站
查看>>
python学习 第一天
查看>>
根据毫秒数计算出当前的“年/月/日/时/分/秒/星期”并不是件容易的事
查看>>