AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 36603
Accepted
Hannah Vernon
Hannah Vernon
Asked: 2013-03-14 09:25:47 +0800 CST2013-03-14 09:25:47 +0800 CST 2013-03-14 09:25:47 +0800 CST

Manipulando o acesso simultâneo a uma tabela de chaves sem deadlocks no SQL Server

  • 772

Eu tenho uma tabela que é usada por um aplicativo legado como substituto de IDENTITYcampos em várias outras tabelas.

Cada linha na tabela armazena o último ID usado LastIDpara o campo nomeado em IDName.

Ocasionalmente, o proc armazenado obtém um impasse - acredito que construí um manipulador de erros apropriado; no entanto, estou interessado em ver se esta metodologia funciona como eu acho que funciona, ou se estou latindo para a árvore errada aqui.

Estou bastante certo de que deve haver uma maneira de acessar esta tabela sem nenhum impasse.

O próprio banco de dados é configurado com READ_COMMITTED_SNAPSHOT = 1.

Primeiro, aqui está a tabela:

CREATE TABLE [dbo].[tblIDs](
    [IDListID] [int] NOT NULL 
        CONSTRAINT PK_tblIDs 
        PRIMARY KEY CLUSTERED 
        IDENTITY(1,1) ,
    [IDName] [nvarchar](255) NULL,
    [LastID] [int] NULL,
);

E o índice não clusterizado no IDNamecampo:

CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName] 
ON [dbo].[tblIDs]
(
    [IDName] ASC
) 
WITH (
    PAD_INDEX = OFF
    , STATISTICS_NORECOMPUTE = OFF
    , SORT_IN_TEMPDB = OFF
    , DROP_EXISTING = OFF
    , ONLINE = OFF
    , ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON
    , FILLFACTOR = 80
);

GO

Alguns dados de exemplo:

INSERT INTO tblIDs (IDName, LastID) 
    VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID) 
    VALUES ('SomeOtherTestID', 1);
GO

O procedimento armazenado usado para atualizar os valores armazenados na tabela e retornar o próximo ID:

CREATE PROCEDURE [dbo].[GetNextID](
    @IDName nvarchar(255)
)
AS
BEGIN
    /*
        Description:    Increments and returns the LastID value from tblIDs
        for a given IDName
        Author:         Hannah Vernon
        Date:           2012-07-19
    */

    DECLARE @Retry int;
    DECLARE @EN int, @ES int, @ET int;
    SET @Retry = 5;
    DECLARE @NewID int;
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    SET NOCOUNT ON;
    WHILE @Retry > 0
    BEGIN
        BEGIN TRY
            BEGIN TRANSACTION;
            SET @NewID = COALESCE((SELECT LastID 
                FROM tblIDs 
                WHERE IDName = @IDName),0)+1;
            IF (SELECT COUNT(IDName) 
                FROM tblIDs 
                WHERE IDName = @IDName) = 0 
                    INSERT INTO tblIDs (IDName, LastID) 
                    VALUES (@IDName, @NewID)
            ELSE
                UPDATE tblIDs 
                SET LastID = @NewID 
                WHERE IDName = @IDName;
            COMMIT TRANSACTION;
            SET @Retry = -2; /* no need to retry since the operation completed */
        END TRY
        BEGIN CATCH
            IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
                SET @Retry = @Retry - 1;
            ELSE
                BEGIN
                SET @Retry = -1;
                SET @EN = ERROR_NUMBER();
                SET @ES = ERROR_SEVERITY();
                SET @ET = ERROR_STATE()
                RAISERROR (@EN,@ES,@ET);
                END
            ROLLBACK TRANSACTION;
        END CATCH
    END
    IF @Retry = 0 /* must have deadlock'd 5 times. */
    BEGIN
        SET @EN = 1205;
        SET @ES = 13;
        SET @ET = 1
        RAISERROR (@EN,@ES,@ET);
    END
    ELSE
        SELECT @NewID AS NewID;
END
GO

Exemplos de execuções do proc armazenado:

EXEC GetNextID 'SomeTestID';

NewID
2

EXEC GetNextID 'SomeTestID';

NewID
3

EXEC GetNextID 'SomeOtherTestID';

NewID
2

EDITAR:

Adicionei um novo índice, pois o índice existente IX_tblIDs_Name não está sendo usado pelo SP; Suponho que o processador de consulta esteja usando o índice clusterizado, pois precisa do valor armazenado em LastID. De qualquer forma, este índice é usado pelo plano de execução real:

CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID 
ON dbo.tblIDs
(
    IDName ASC
) 
INCLUDE
(
    LastID
)
WITH (FILLFACTOR = 100
    , ONLINE=ON
    , ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON);

EDIÇÃO #2:

Eu segui o conselho que @AaronBertrand deu e modifiquei um pouco. A ideia geral aqui é refinar a declaração para eliminar bloqueios desnecessários e, em geral, tornar o SP mais eficiente.

O código abaixo substitui o código acima de BEGIN TRANSACTIONto END TRANSACTION:

BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID 
        FROM dbo.tblIDs 
        WHERE IDName = @IDName), 0) + 1;

IF @NewID = 1
    INSERT INTO tblIDs (IDName, LastID) 
    VALUES (@IDName, @NewID);
ELSE
    UPDATE dbo.tblIDs 
    SET LastID = @NewID 
    WHERE IDName = @IDName;

COMMIT TRANSACTION;

Como nosso código nunca adiciona um registro a esta tabela com 0 LastID, podemos supor que, se @NewID for 1, a intenção é anexar um novo ID à lista, caso contrário, estamos atualizando uma linha existente na lista.

sql-server deadlock
  • 5 5 respostas
  • 32848 Views

5 respostas

  • Voted
  1. Best Answer
    A-K
    2013-03-16T12:37:26+08:002013-03-16T12:37:26+08:00

    First, I would avoid making a round trip to the database for every value. For example, if your application knows it needs 20 new IDs, do not make 20 round trips. Make only one stored procedure call, and increment the counter by 20. Also it might be better to split your table into multiple ones.

    It is possible to avoid deadlocks altogether. I have no deadlocks at all in my system. There are several ways to accomplish that. I will show how I would use sp_getapplock to eliminate deadlocks. I have no idea if this will work for you, because SQL Server is closed source, so I cannot see the source code, and as such I do not know if I have tested all possible cases.

    The following describes what works for me. YMMV.

    First, let us start with a scenario where we always get a considerable amount of deadlocks. Second, we shall use sp_getapplock eliminate them. The most important point here is to stress test your solution. Your solution may be different, but you need to expose it to high concurrency, as I will demonstrate later.

    Prerequisites

    Let us set up a table with some test data:

    CREATE TABLE dbo.Numbers(n INT NOT NULL PRIMARY KEY); 
    GO 
    
    INSERT INTO dbo.Numbers 
        ( n ) 
            VALUES  ( 1 ); 
    GO 
    DECLARE @i INT; 
        SET @i=0; 
    WHILE @i<21  
        BEGIN 
        INSERT INTO dbo.Numbers 
            ( n ) 
            SELECT n + POWER(2, @i) 
            FROM dbo.Numbers; 
        SET @i = @i + 1; 
        END;  
    GO
    
    SELECT n AS ID, n AS Key1, n AS Key2, 0 AS Counter1, 0 AS Counter2
    INTO dbo.DeadlockTest FROM dbo.Numbers
    GO
    
    ALTER TABLE dbo.DeadlockTest ADD CONSTRAINT PK_DeadlockTest PRIMARY KEY(ID);
    GO
    
    CREATE INDEX DeadlockTestKey1 ON dbo.DeadlockTest(Key1);
    GO
    
    CREATE INDEX DeadlockTestKey2 ON dbo.DeadlockTest(Key2);
    GO
    

    The following two procedures are quite likely to embrace in a deadlock:

    CREATE PROCEDURE dbo.UpdateCounter1 @Key1 INT
    AS
    SET NOCOUNT ON ;
    SET XACT_ABORT ON;
    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN TRANSACTION ;
    UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
    SET @Key1=@Key1-10000;
    UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
    COMMIT;
    GO
    
    CREATE PROCEDURE dbo.UpdateCounter2 @Key2 INT
    AS
    SET NOCOUNT ON ;
    SET XACT_ABORT ON;
    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN TRANSACTION ;
    SET @Key2=@Key2-10000;
    UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
    SET @Key2=@Key2+10000;
    UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
    COMMIT;
    GO
    

    Reproducing deadlocks

    The following loops should reproduce more than 20 deadlocks every time you run them. If you get less than 20, increase the number of iterations.

    In one tab, run this;

    DECLARE @i INT, @DeadlockCount INT;
    SELECT @i=0, @DeadlockCount=0;
    
    WHILE @i<5000 BEGIN ;
      BEGIN TRY 
        EXEC dbo.UpdateCounter1 @Key1=123456;
      END TRY
      BEGIN CATCH
        SET @DeadlockCount = @DeadlockCount + 1;
        ROLLBACK;
      END CATCH ;
      SET @i = @i + 1;
    END;
    SELECT 'Deadlocks caught: ', @DeadlockCount ;
    

    In another tab, run this script.

    DECLARE @i INT, @DeadlockCount INT;
    SELECT @i=0, @DeadlockCount=0;
    
    WHILE @i<5000 BEGIN ;
      BEGIN TRY 
        EXEC dbo.UpdateCounter2 @Key2=123456;
      END TRY
      BEGIN CATCH
        SET @DeadlockCount = @DeadlockCount + 1;
        ROLLBACK;
      END CATCH ;
      SET @i = @i + 1;
    END;
    SELECT 'Deadlocks caught: ', @DeadlockCount ;
    

    Make sure you start both within a couple of seconds.

    Using sp_getapplock to eliminate deadlocks

    Alter both procedures, rerun the loop, and see that you no longer have deadlocks:

    ALTER PROCEDURE dbo.UpdateCounter1 @Key1 INT
    AS
    SET NOCOUNT ON ;
    SET XACT_ABORT ON;
    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN TRANSACTION ;
    EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
    UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
    SET @Key1=@Key1-10000;
    UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
    COMMIT;
    GO
    
    ALTER PROCEDURE dbo.UpdateCounter2 @Key2 INT
    AS
    SET NOCOUNT ON ;
    SET XACT_ABORT ON;
    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN TRANSACTION ;
    EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
    SET @Key2=@Key2-10000;
    UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
    SET @Key2=@Key2+10000;
    UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
    COMMIT;
    GO
    

    Using a table with one row to eliminate deadlocks

    Instead of invoking sp_getapplock, we can modify the following table:

    CREATE TABLE dbo.DeadlockTestMutex(
    ID INT NOT NULL,
    CONSTRAINT PK_DeadlockTestMutex PRIMARY KEY(ID),
    Toggle INT NOT NULL);
    GO
    
    INSERT INTO dbo.DeadlockTestMutex(ID, Toggle)
    VALUES(1,0);
    

    Once we have this table created and populated, we can replace the following line

    EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
    

    with this one, in both procedures:

    UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
    

    You can rerun the stress test, and see for yourself that we have no deadlocks.

    Conclusion

    As we have seen, sp_getapplock can be used to serialize access to other resources. As such it can be used to eliminate deadlocks.

    Of course, this can significantly slow down modifications. To address that, we need to choose the right granularity for the exclusive lock, and whenever possible, work with sets instead of individual rows.

    Antes de usar essa abordagem, você precisa fazer um teste de estresse você mesmo. Primeiro, você precisa ter certeza de obter pelo menos algumas dúzias de impasses com sua abordagem original. Segundo, você não deve obter deadlocks ao executar novamente o mesmo script de reprodução usando o procedimento armazenado modificado.

    Em geral, não acho que haja uma boa maneira de determinar se seu T-SQL está protegido contra deadlocks apenas observando-o ou observando o plano de execução. IMO a única maneira de determinar se o seu código é propenso a deadlocks é expô-lo a alta simultaneidade.

    Boa sorte com a eliminação de impasses! Não temos nenhum impasse em nosso sistema, o que é ótimo para nosso equilíbrio entre vida profissional e pessoal.

    • 15
  2. Mark Storey-Smith
    2013-03-14T11:57:05+08:002013-03-14T11:57:05+08:00

    O uso da XLOCKdica em sua SELECTabordagem ou no seguinte UPDATEdeve ser imune a esse tipo de impasse:

    DECLARE @Output TABLE ([NewId] INT);
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    
    BEGIN TRANSACTION;
    
    UPDATE
        dbo.tblIDs WITH (XLOCK)
    SET 
        LastID = LastID + 1
    OUTPUT
        INSERTED.[LastId] INTO @Output
    WHERE
        IDName = @IDName;
    
    IF(@@ROWCOUNT = 1)
    BEGIN
        SELECT @NewId = [NewId] FROM @Output;
    END
    ELSE
    BEGIN
        SET @NewId = 1;
    
        INSERT dbo.tblIDs
            (IDName, LastID)
        VALUES
            (@IDName, @NewId);
    END
    
    SELECT [NewId] = @NewId ;
    
    COMMIT TRANSACTION;
    

    Voltará com algumas outras variantes (se não for derrotado!).

    • 9
  3. Hannah Vernon
    2013-03-15T12:26:01+08:002013-03-15T12:26:01+08:00

    Mike Defehr me mostrou uma maneira elegante de fazer isso de uma maneira muito leve:

    ALTER PROCEDURE [dbo].[GetNextID](
        @IDName nvarchar(255)
    )
    AS
    BEGIN
        /*
            Description:    Increments and returns the LastID value from tblIDs for a given IDName
            Author:         Hannah Vernon / Mike Defehr
            Date:           2012-07-19
    
        */
    
        DECLARE @Retry int;
        DECLARE @EN int, @ES int, @ET int;
        SET @Retry = 5;
        DECLARE @NewID int;
        SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
        SET NOCOUNT ON;
        WHILE @Retry > 0
        BEGIN
            BEGIN TRY
                UPDATE dbo.tblIDs 
                SET @NewID = LastID = LastID + 1 
                WHERE IDName = @IDName;
    
                IF @NewID IS NULL
                BEGIN
                    SET @NewID = 1;
                    INSERT INTO tblIDs (IDName, LastID) VALUES (@IDName, @NewID);
                END
                SET @Retry = -2; /* no need to retry since the operation completed */
            END TRY
            BEGIN CATCH
                IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
                    SET @Retry = @Retry - 1;
                ELSE
                    BEGIN
                    SET @Retry = -1;
                    SET @EN = ERROR_NUMBER();
                    SET @ES = ERROR_SEVERITY();
                    SET @ET = ERROR_STATE()
                    RAISERROR (@EN,@ES,@ET);
                    END
            END CATCH
        END
        IF @Retry = 0 /* must have deadlock'd 5 times. */
        BEGIN
            SET @EN = 1205;
            SET @ES = 13;
            SET @ET = 1
            RAISERROR (@EN,@ES,@ET);
        END
        ELSE
            SELECT @NewID AS NewID;
    END
    GO
    

    (Para completar, aqui está a tabela associada ao proc armazenado)

    CREATE TABLE [dbo].[tblIDs]
    (
        IDName nvarchar(255) NOT NULL,
        LastID int NULL,
        CONSTRAINT [PK_tblIDs] PRIMARY KEY CLUSTERED 
        (
            [IDName] ASC
        ) WITH 
        (
            PAD_INDEX = OFF
            , STATISTICS_NORECOMPUTE = OFF
            , IGNORE_DUP_KEY = OFF
            , ALLOW_ROW_LOCKS = ON
            , ALLOW_PAGE_LOCKS = ON
            , FILLFACTOR = 100
        ) 
    );
    GO
    

    Este é o plano de execução para a versão mais recente:

    insira a descrição da imagem aqui

    E este é o plano de execução para a versão original (suscetível a deadlock):

    insira a descrição da imagem aqui

    Claramente, a nova versão ganha!

    Para efeito de comparação, a versão intermediária com o (XLOCK)etc, produz o seguinte plano:

    insira a descrição da imagem aqui

    Eu diria que é uma vitória! Obrigado pela ajuda de todos!

    • 7
  4. Mike DeFehr
    2013-03-16T06:00:22+08:002013-03-16T06:00:22+08:00

    Not to steal Mark Storey-Smith's thunder, but he is onto something with his post above (that has incidentally received the most upvotes). The advice I gave Hannah was centered around the "UPDATE set @variable = column = column + value" construct which I find really cool, but I think may be undocumented (it has to be supported, though because it is there specifically for the TCP benchmarks).

    Here is a variation of Mark's answer - because you are returning the new ID value as a recordset, you can do away with the scalar variable entirely, no explicit transaction should be necessary either, and I would agree that messing around with isolation levels is unnecessary as well. The result is very clean and pretty slick...

    ALTER PROC [dbo].[GetNextID]
      @IDName nvarchar(255)
      AS
    BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Output TABLE ([NewID] INT);
    
    UPDATE dbo.tblIDs SET LastID = LastID + 1
    OUTPUT inserted.[LastId] INTO @Output
    WHERE IDName = @IDName;
    
    IF(@@ROWCOUNT = 1)
        SELECT [NewID] FROM @Output;
    ELSE
        INSERT dbo.tblIDs (IDName, LastID)
        OUTPUT INSERTED.LastID AS [NewID]
        VALUES (@IDName,1);
    END
    
    • 6
  5. Aaron Bertrand
    2013-03-14T09:31:11+08:002013-03-14T09:31:11+08:00

    Corrigi um impasse semelhante em um sistema no ano passado, alterando isso:

    IF (SELECT COUNT(IDName) FROM tblIDs WHERE IDName = @IDName) = 0 
      INSERT INTO tblIDs (IDName, LastID) VALUES (@IDName, @NewID)
    ELSE
      UPDATE tblIDs SET LastID = @NewID WHERE IDName = @IDName;
    

    Para isso:

    UPDATE tblIDs SET LastID = @NewID WHERE IDName = @IDName;
    IF @@ROWCOUNT = 0
    BEGIN
      INSERT ...
    END
    

    Em geral, selecionar um COUNTapenas para determinar presença ou ausência é um desperdício. Nesse caso, como é 0 ou 1, não é como se fosse muito trabalho, mas (a) esse hábito pode sangrar em outros casos em que será muito mais caro (nesses casos, use IF NOT EXISTSem vez de IF COUNT() = 0), e (b) a varredura adicional é completamente desnecessária. O UPDATEexecuta essencialmente a mesma verificação.

    Além disso, isso parece um cheiro de código sério para mim:

    SET @NewID = COALESCE((SELECT LastID FROM tblIDs WHERE IDName = @IDName),0)+1;
    

    Qual é o ponto aqui? Por que não usar apenas uma coluna de identidade ou derivar essa sequência usando ROW_NUMBER()no momento da consulta?

    • 4

relate perguntas

  • SQL Server - Como as páginas de dados são armazenadas ao usar um índice clusterizado

  • Preciso de índices separados para cada tipo de consulta ou um índice de várias colunas funcionará?

  • Quando devo usar uma restrição exclusiva em vez de um índice exclusivo?

  • Quais são as principais causas de deadlocks e podem ser evitadas?

  • Como determinar se um Índice é necessário ou necessário

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Como fazer a saída do sqlplus aparecer em uma linha?

    • 3 respostas
  • Marko Smith

    Selecione qual tem data máxima ou data mais recente

    • 3 respostas
  • Marko Smith

    Como faço para listar todos os esquemas no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Conceder acesso a todas as tabelas para um usuário

    • 5 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Stéphane Como faço para listar todos os esquemas no PostgreSQL? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    pedrosanta Listar os privilégios do banco de dados usando o psql 2011-08-04 11:01:21 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve