How to get the next number in a sequence

前端 未结 9 1079
死守一世寂寞
死守一世寂寞 2020-12-04 01:48

I have a table like this:

+----+-----------+------+-------+--+
| id | Part      | Seq  | Model |  |
+----+-----------+------+-------+--+
| 1  | Head      | 0         


        
9条回答
  •  囚心锁ツ
    2020-12-04 02:24

    Let's first list the challenges:

    1. We cannot use a normal constraint as there are existing null values and we also need to cater for duplicates as well as gaps - if we look at the existing data. This is fine, we will figure it out ;-> in step 3
    2. We require safety for concurrent operations (thus some form or mix of transactions, isolation levels and possibly a "kinda SQL mutex".) Gut feel here is a stored proc for a couple of reasons:

      2.1 It protects more easily from sql injection

      2.2 We can control the isolation levels (table locking) more easily and recover from some issues which come with this kind of requirement

      2.3 We can use application level db locks to control the concurrency

    3. We must store or find the next value on every insert. The word concurrency tells us already that there will be contention and probably high throughput (else please stick to single threads). So we must already be thinking: do not read from the same table you want to write to in an already complicated world.

    So with that short prequel, let's attempt a solution:

    As a start, we are creating your original table and then also a table to hold the sequence (BodyPartsCounter) which we are setting to the last used sequence + 1:

        CREATE TABLE BodyParts
            ([id] int identity, [Part] varchar(9), [Seq] varchar(4), [Model] int)
        ;
    
        INSERT INTO BodyParts
            ([Part], [Seq], [Model])
        VALUES
            ('Head', NULL, 3),
            ('Neck', '1', 3),
            ('Shoulders', '2', 29),
            ('Shoulders', '2', 3),
            ('Stomach', '5', 3)
        ;
    
        CREATE TABLE BodyPartsCounter
            ([id] int
            , [counter] int)
        ;
    
        INSERT INTO BodyPartsCounter
            ([id], [counter])
        SELECT 1, MAX(id) + 1 AS id FROM BodyParts
        ;
    

    Then we need to create the stored procedure which will do the magic. In short, it acts as a mutex, basically guaranteeing you concurrency (if you do not do inserts or updates into the same tables elsewhere). It then get's the next seq, updates it and inserts the new row. After this has all happened it will commit the transaction and release the stored proc for the next waiting calling thread.

    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO
    -- =============================================
    -- Author:      Charlla
    -- Create date: 2016-02-15
    -- Description: Inserts a new row in a concurrently safe way
    -- =============================================
    CREATE PROCEDURE InsertNewBodyPart 
    @bodypart varchar(50), 
    @Model int = 3
    AS
    BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    
        BEGIN TRANSACTION;
    
        -- Get an application lock in your threaded calls
        -- Note: this is blocking for the duration of the transaction
        DECLARE @lockResult int;
        EXEC @lockResult = sp_getapplock @Resource = 'BodyPartMutex', 
                       @LockMode = 'Exclusive';
        IF @lockResult = -3 --deadlock victim
        BEGIN
            ROLLBACK TRANSACTION;
        END
        ELSE
        BEGIN
            DECLARE @newId int;
            --Get the next sequence and update - part of the transaction, so if the insert fails this will roll back
            SELECT @newId = [counter] FROM BodyPartsCounter WHERE [id] = 1;
            UPDATE BodyPartsCounter SET [counter] = @newId + 1 WHERE id = 1;
    
            -- INSERT THE NEW ROW
            INSERT INTO dbo.BodyParts(
                Part
                , Seq
                , Model
                )
                VALUES(
                    @bodypart
                    , @newId
                    , @Model
                )
            -- END INSERT THE NEW ROW
            EXEC @lockResult = sp_releaseapplock @Resource = 'BodyPartMutex';
            COMMIT TRANSACTION;
        END;
    
    END
    GO
    

    Now run the test with this:

    EXEC    @return_value = [dbo].[InsertNewBodyPart]
        @bodypart = N'Stomach',
        @Model = 4
    
    SELECT  'Return Value' = @return_value
    
    SELECT * FROM BodyParts;
    SELECT * FROM BodyPartsCounter
    

    This all works - but be careful - there's a lot to consider with any kind of multithreaded app.

    Hope this helps!

提交回复
热议问题