1

I'm a bit perplexed at a situation where I lock wait timeout when trying to insert columns into a database using Java's PreparedStatements, but don't have a similar issue if I do it with MySQL.

The two tables I'm dealing with are named items and inventory_items. Inventory_items has a foreign key reference to items for the column inventoryitemid.

What I currently have in Java is that a batch insert is made to items and I get the returned keys, then insert the relevant information into inventory_items. There are other tables that have the same foreign key references to items, but for this case if I can fix the issue between items and inventory_items, then I'm sure I can fix it for the rest.

The table structure for inventory_items

CREATE TABLE `inventory_items` (
  `inventoryitemid` int(11) unsigned NOT NULL,
  `characterid` int(11) DEFAULT NULL,
  `inventorytype` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`inventoryitemid`),
  KEY `charid` (`characterid`),
  CONSTRAINT `INVENTORYITEM_FK` FOREIGN KEY (`inventoryitemid`) REFERENCES `items` (`inventoryitemid`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1

The table structure for items

CREATE TABLE `items` (
  `inventoryitemid` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `itemid` int(11) NOT NULL DEFAULT '0',
  `quantity` int(11) NOT NULL DEFAULT '0',
  `owner` tinytext NOT NULL,
  `petid` int(11) NOT NULL DEFAULT '-1',
  `flag` int(11) NOT NULL,
  `expiration` bigint(20) NOT NULL DEFAULT '-1',
  `giftFrom` varchar(26) NOT NULL,
  `position` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`inventoryitemid`)
) ENGINE=InnoDB AUTO_INCREMENT=733 DEFAULT CHARSET=latin1

Image for items

enter image description here

Image for inventory_items

enter image description here

Java Prepared Statement (times out)

con.prepareStatement("INSERT INTO inventory_items (inventoryitemid, characterid, inventorytype) VALUES (?, ?, ?)");

MySQL equivalent

INSERT INTO inventory_items (inventoryitemid, characterid, inventorytype) VALUES (730, 38843, 3)

Note that I had inputted what would have been the actual values in the SQL statement to see if it could insert.

Full Java SQL Code

@Override
        public void batch(Pair<Item, MapleInventoryType> entry, int key, int id) {
            try {
                if (ps.isClosed())
                    ps = con.prepareStatement("INSERT INTO inventory_items (inventoryitemid, characterid, inventorytype) VALUES (?, ?, ?)");
                //System.out.println("Batching! " + ps.isClosed());
                System.out.println("KEY IS " + key);
                ps.setInt(1, key);
                ps.setInt(2, id);
                ps.setInt(3, entry.getRight().getType());
                ps.addBatch();
            } catch (Exception e) {
                LogHelper.GENERAL_EXCEPTION.get().info("InventoryWorker batch: " + e);
            }

        }

        @Override
        public void executeBatch(int id) throws SQLException {
            // TODO Auto-generated method stub
            try (PreparedStatement delete = con.prepareStatement("DELETE FROM items WHERE EXISTS (SELECT * FROM inventory_items WHERE inventory_items.inventoryitemid = items.inventoryitemid AND characterid = ?)")) {
                System.out.println("DELETE BY " + id); 
                delete.setInt(1, id);
                delete.execute(); 

                if (!ps.isClosed()) {
                    ps.executeBatch();
                }
            } catch (SQLException e) {
                LogHelper.GENERAL_EXCEPTION.get().info("InventoryWorker executeBatch: " + e);
            } finally {
                ps.close();
            }
        }

The reason why I have it split into two methods is because there are different types of savings I am doing, so I split the saving up into classes to handle types of saving. I don't think we should be concerned about the possibility of the prepared statement never closing as well since I have a finally clause outside of this scope that closes everything just in case of an exception.

Entire save clause that calls the above 2 methods. Note that those two methods are under a class called worker. In the code it will be called as worker.batch() and worker.executeBatch()

    public void saveItems(List<Pair<Item, MapleInventoryType>> items, int id, Connection con) throws SQLException {
        lock.lock();
        long start = System.currentTimeMillis();
//        StringBuilder query = new StringBuilder();
//        query.append(
//                "UPDATE `inventoryitems` SET characterid = -1 WHERE `");
//        query.append(account ? "accountid" : "characterid").append("` = ? AND `type` = ? LIMIT 128");
        try {
//            try (PreparedStatement ps = con.prepareStatement(query.toString())) {                
//                ps.setInt(1, id);
//                ps.setInt(2, value);
//                ps.executeUpdate();
//            } catch (Exception e) {
//              LogHelper.GENERAL_EXCEPTION.get().info("ItemFactory updateItems: " + e);
//            }
            System.out.println("Time to delete items " + (System.currentTimeMillis() - start));
            start = System.currentTimeMillis();
            try (PreparedStatement ps = con.prepareStatement("INSERT INTO `items` (`itemid`"+
                    ", `quantity`, `owner`, `petid`, `flag`, `expiration`, `giftFrom`, `position`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
                 Statement.RETURN_GENERATED_KEYS);
                 PreparedStatement pse = con.prepareStatement(
                 "INSERT INTO `inventoryequipment` (`inventoryequipmentid`, `inventoryitemid`, `upgradeslots`, `level`, `str`, `dex`, `int`"+
                 ", `luk`, `hp`, `mp`, `watk`, `matk`, `wdef`, `mdef`, `acc`, `avoid`, `hands`, `speed`, `jump`, `vicious`, `itemLevel`, `itemexp`, `ringid`, `skill`) VALUES (DEFAULT, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
                ) {

                LinkedList<Pair<Item, MapleInventoryType>> saved_items = new LinkedList<>();

                if (!items.isEmpty()) {
                    for (Pair<Item, MapleInventoryType> pair : items) {
                        Item item = pair.getLeft();
                        if (item.disappearsAtLogout())
                            continue;
                        MapleInventoryType mit = pair.getRight();
                        ps.setInt(1, item.getItemId());
                        ps.setInt(2, item.getQuantity());
                        ps.setString(3, item.getOwner());
                        ps.setInt(4, item.getPetId());
                        ps.setInt(5, item.getFlag());
                        ps.setLong(6, item.getExpiration());
                        ps.setString(7, item.getGiftFrom());
                        ps.setInt(8, item.getPosition());
//                        ps.setInt(1, value);
//                        ps.setString(2, account ? null : String.valueOf(id));
//                        ps.setString(3, account ? String.valueOf(id) : null);
//                        ps.setInt(4, item.getItemId());
//                        ps.setInt(5, mit.getType());
//                        ps.setInt(6, item.getPosition());
//                        ps.setInt(7, item.getQuantity());
//                        ps.setString(8, item.getOwner());
//                        ps.setInt(9, item.getPetId());
//                        ps.setInt(10, item.getFlag());
//                        ps.setLong(11, item.getExpiration());
//                        ps.setString(12, item.getGiftFrom());
                        saved_items.add(pair);
                        ps.addBatch();
                    }

                    ps.executeBatch();
                    System.out.println("Time to save items " + (System.currentTimeMillis() - start));
                    start = System.currentTimeMillis();
                    System.out.println("RIGHT BEFORE EQUIPMENT");
                    try (ResultSet rs = ps.getGeneratedKeys()) {
                        for (Pair<Item, MapleInventoryType> item : saved_items) {  
                            int key = -1;
                            if (rs.next()) {                            
                                key = rs.getInt(1);
                            }

                            if (key == 0) {
                                key = item.getLeft().getInventoryItemId();
                            }

                            worker.batch(item, key, id);
                            MapleInventoryType mit = item.right; 
                            if (mit.equals(MapleInventoryType.EQUIP) || mit.equals(MapleInventoryType.EQUIPPED)) {
                                pse.setInt(1, key);
                                Equip equip = (Equip) item.left;
                                pse.setInt(2, equip.getUpgradeSlots());
                                pse.setInt(3, equip.getLevel());
                                pse.setInt(4, equip.getStr());
                                pse.setInt(5, equip.getDex());
                                pse.setInt(6, equip.getInt());
                                pse.setInt(7, equip.getLuk());
                                pse.setInt(8, equip.getHp());
                                pse.setInt(9, equip.getMp());
                                pse.setInt(10, equip.getWatk());
                                pse.setInt(11, equip.getMatk());
                                pse.setInt(12, equip.getWdef());
                                pse.setInt(13, equip.getMdef());
                                pse.setInt(14, equip.getAcc());
                                pse.setInt(15, equip.getAvoid());
                                pse.setInt(16, equip.getHands());
                                pse.setInt(17, equip.getSpeed());
                                pse.setInt(18, equip.getJump());
                                pse.setInt(19, equip.getVicious());
                                pse.setInt(20, equip.getItemLevel());
                                pse.setInt(21, equip.getItemExp());
                                pse.setInt(22, equip.getRingId());
                                pse.setBoolean(23, equip.hasSkill());
                                pse.addBatch(); 
                            }
                        }
                        pse.executeBatch();
                    }
                }
                worker.executeBatch(id);
            }
            System.out.println("Time to save equipment " + (System.currentTimeMillis() - start));
        } catch (Exception e) {
            LogHelper.GENERAL_EXCEPTION.get().info("ItemFactory saveItems: " + e);
        } finally {
            worker.close();
            lock.unlock();
        }
    }
Jacob Macallan
  • 959
  • 2
  • 8
  • 29
  • Where is the actual query/statement? MySQL does not have a concept of prepared statement AFAIK. – Tim Biegeleisen May 10 '17 at 03:55
  • Okay, I'll update the post. One second! – Jacob Macallan May 10 '17 at 03:55
  • @TimBiegeleisen I updated the post with the appropriate statements. Note that the MySQL one is its equivalent in Java, just with the data pre-inserted. – Jacob Macallan May 10 '17 at 03:57
  • Then you built the prepared statement incorrectly in Java. Include the full Java code and let's fix this. – Tim Biegeleisen May 10 '17 at 03:58
  • @TimBiegeleisen That's almost all the code directly involved in the saving. If you actually want the whole piece I can link it, but it's > 150 lines in total since it encompasses different saves as well. – Jacob Macallan May 10 '17 at 04:02
  • You never call `ps.executeBatch()` when inserting. Why would you expect the insert to work at all? – Tim Biegeleisen May 10 '17 at 04:06
  • @TimBiegeleisen It's split into two different methods. The batch part adds the stuff to be batched; executeBatch actually executes the whole thing. I'll include the rest of the code to make it clearer, but it is a bit of an eyesore. – Jacob Macallan May 10 '17 at 04:08
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/143824/discussion-between-jonathan-nguyen-and-tim-biegeleisen). – Jacob Macallan May 10 '17 at 04:11
  • I can't see the exact problem(s) in your code, but it is probably due to your abuse of batching. Shrink the code down until it starts working, then add back batching. There is nothing wrong with Java's prepared statement API. – Tim Biegeleisen May 10 '17 at 04:13
  • @TimBiegeleisen That's probably the best choice. I'll give it a run and see what happens. I do want to note however that I got the same results even if I changed the addBatch to a simple executeUpdate() instead. – Jacob Macallan May 10 '17 at 04:15
  • 1
    If I were using batching, I would create a method which accepts a statement and the parameters to be added to the `INSERT`. But your batching method _creates_ a statement. This seems like a poor design choice to me. Instead, just let it add another insert to the batch. – Tim Biegeleisen May 10 '17 at 04:17
  • @JonathanNguyen Lock wait is often a problem with transactions. Any chance the java version has more than one overlapping transaction? – D.L. May 10 '17 at 04:19
  • @JonathanNguyen If you remove (just as a test) the fk constraint from inventory items, and the problem goes away that would support what I'm thinking. – D.L. May 10 '17 at 04:22
  • @D.L. I tried that earlier and it definitely did go away. My question then was: why does it cause issues when executed in Java, but not when the similar statement is executed in MySQL? I create the parent first and then the child, but it still gives issues, so I'm stuck there. I'm trying to redesign it a little, like Tim said because the design is a bit funky. – Jacob Macallan May 10 '17 at 04:30
  • My guess is because in Java you're leaving a transaction open, but when you run it from the mysql client, the transactions are auto-committing. – D.L. May 10 '17 at 04:32
  • @D.L.What would the solution be in this case then? – Jacob Macallan May 10 '17 at 04:36
  • Could be because you don't close the PreparedStatement? Don't remember if you have to do that. Pretty sure you do with regular statements though. – D.L. May 10 '17 at 04:36
  • Sorry, having a little trouble following your code, but the reason you get the lock wait timeout message is because a SQL command attempts to get a lock on a row but has to wait more than 50 seconds for another command to release the lock. Because you have a constraint between the two tables, a modification to one table often ends up getting row locks in both tables. Because removing the constraint fixes the problem, I suspect you are modifying inventory_items and items simultaneously in a way that mysql doesn't like in your java code. – D.L. May 10 '17 at 04:50
  • 1
    @JonathanNguyen Here's a possible way to debug it. Modify one table in java running a debugger so you can step through it. Modify the other table on the command line. (It would have to be a row related by the constraint, of course.) By stepping through the code a line at a time in the debugger, you should be able to tell exactly when the row lock is released because the command line should block until it is. – D.L. May 10 '17 at 04:59
  • @D.L. Hmmm. I think I managed to fix the issue by simply having a pre-instantiated PreparedStatement instead of creating one each time inside the methods. That seems to have cleared up the locking issues. – Jacob Macallan May 10 '17 at 06:27
  • Hmmm... Not sure if the details are exactly the same, but sounds similar: http://stackoverflow.com/questions/12977568/two-different-prepared-statements-in-one-single-batch. Glad you were able to solve it. – D.L. May 10 '17 at 07:32

0 Answers0