处理来自客户端的多个请求,在Spring应用程序中更新表中的列。

huangapple go评论72阅读模式
英文:

Handling multiple request from client to update a column in table in spring application

问题

在表格中,我有一个名为“Balance”的表格,其中包含员工和员工积分。

当员工John同时使用移动应用程序和Web应用程序并在同一时间下订单时,当前情况下应相应地更新积分。

当他尝试同时下订单时,每个请求都会获得5000积分,根据John的购物车价值更新积分。

从移动应用程序中,他兑换了2000积分,因此更新后的金额为5000-2000,即3000。
从Web应用程序中,他兑换了1000积分,因此更新后的金额应为3000-1000,即2000。

但在当前情况下,更新后的金额为5000-1000 = 4000(用于Web应用程序的脏读取)。

为确保事务正确,我应该怎么做?

我已将隔离级别设置为SERIALIZABLE,但在这种情况下,当第二个请求更新表列“points”时,我会收到org.springframework.dao.DeadlockLoserDataAccessException:

为确保两个同时的请求都正确更新表格的积分,我应该怎么做?

以下是我的代码:

@Transactional(isolation=Isolation.SERIALIZABLE)
public void updatePoints(String employee,int points){
	Object[] arr = new Object[] {points, employeeId};
	getJdbcTemplate().update("UPDATE BALANCE SET POINTS =POINTS -? WHERE EMPLOYEEID=?", arr);
}

请注意,代码仅用于理解目的,事务性在服务层而不是DAO层使用:

@Override
@Transactional(isolation=Isolation.SERIALIZABLE)
public void putFinalCartItems(String employeeCode, List<String> cartId, Map<String, Object> statusMap,int userId) {
    CartItems carPoints = repoDao.getFinalCartItems(cartId,employeeCode);//here i have used select query with join from Balance Table
    try {
        String orderNo=null;
        String mainOrderNo=OrderIdGenerator.getOrderId();
        List<CartItems> finalCartItems = repoDao.getBifurcatedFinalCart(cartId,employeeCode);//here i have used select query with join from Balance Table
        Integer balance = Integer.parseInt(finalCartItems.get(0).getPoints());
        Integer successCount=0;
        try {
            for(CartItems cartEntityId : finalCartItems) {
                balance = balance-Integer.parseInt(cartEntityId.getTotalNoOfPoint());
                orderNo="TRANS-"+cartEntityId.getId();
                successCount = repoDao.placeOrder(cartEntityId);
            }
            repoDao.updatePoints(Integer.parseInt(carPoints.getTotalNoOfPoint()),userId,employeeCode);
        }catch(Exception e) {
            //e.printStackTrace();
            successCount=0;
        }
        if(successCount>0) {
            FinalOrder lfo = new FinalOrder();
            lfo.setBalance(String.valueOf(totbalance));
            lfo.setMainOrderId(mainOrderNo);
            lfo.setTotalPointsSpent(carPoints.getTotalNoOfPoint());
            statusMap.put("success",lfo );
        }
    } catch(Exception e ) {
        statusMap.put("error","Please try again after some time" );
    }
}

错误堆栈跟踪显示在第二个请求上发生的错误:

org.springframework.dao.DeadlockLoserDataAccessException: PreparedStatementCallback; SQL [UPDATE BALANCE set Points=points-? where EMPLOYEEID=? ]; Transaction (Process ID 94) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.; nested exception is com.microsoft.sqlserver.jdbc.SQLServerException: Transaction (Process ID 94) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:263)
...

我希望第二个请求能够在没有脏读取的情况下同时成功。

英文:

I have a table called Balance in that table i have employee and the employee points.

处理来自客户端的多个请求,在Spring应用程序中更新表中的列。

Lets say the employee John is using mobile and web app simultaneously he places order at the same time.

So currently when he tries to place order from both the app the point should be updated accordingly.

When he tries to place order simultaneoulsy each request is getting 5000 point and based on cart value of john the point is updated.

From mobile app he redeeemed 2000 points so the updated amount is 5000-2000 ie 3000
From web app he redeemed 1000 point so the updatded amount should be 3000-1000 ie 2000

But in current scenario the updated amount is 5000-1000 = 4000.(For web app dirty read)

What should i do in order to make sure the transaction is proper.

I have added isolation level to SERIALIZABLE but in this case when second request updates the table column points

I get org.springframework.dao.DeadlockLoserDataAccessException:

What should i do in order to make sure that both the simultaneous request update the table with proper points.

Below is my code

@Transactional(isolation=Isolation.SERIALIZABLE)
public void updatePoints(String employee,int points){
Object[] arr = new Object[] {points, employeeId};
getJdbcTemplate().update(&quot;UPDATE BALANCE SET POINTS =POINTS -? WHERE EMPLOYEEID=?&quot;,arr);
} 

PLEASE FIND UPDTAED CODE ABOVE CODE WAS ONLY FOR UNDERSTANDING PURPOSE
Used Transactional at service layer not in dao layer

@Override
@Transactional(isolation=Isolation.SERIALIZABLE)
public void putFinalCartItems(String employeeCode, List&lt;String&gt; cartId, Map&lt;String, Object&gt; statusMap,int userId) {
CartItems carPoints = repoDao.getFinalCartItems(cartId,employeeCode);//here i have used select query with join from Balance Table
try {
String orderNo=null;
String mainOrderNo=OrderIdGenerator.getOrderId();
List&lt;CartItems&gt; finalCartItems = repoDao.getBifurcatedFinalCart(cartId,employeeCode);//here i have used select query with join from Balance Table
Integer balance = Integer.parseInt(finalCartItems.get(0).getPoints());
Integer successCount=0;
try {
for(CartItems cartEntityId : finalCartItems) {
balance = balance-Integer.parseInt(cartEntityId.getTotalNoOfPoint());
orderNo=&quot;TRANS-&quot;+cartEntityId.getId();
successCount = repoDao.placeOrder(cartEntityId);
}
repoDao.updatePoints(Integer.parseInt(carPoints.getTotalNoOfPoint()),userId,employeeCode);
}catch(Exception e) {
//e.printStackTrace();
successCount=0;
}
if(successCount&gt;0) {
FinalOrder lfo = new FinalOrder();
lfo.setBalance(String.valueOf(totbalance));
lfo.setMainOrderId(mainOrderNo);
lfo.setTotalPointsSpent(carPoints.getTotalNoOfPoint());
statusMap.put(&quot;success&quot;,lfo );
}
}
catch(Exception e ) {
statusMap.put(&quot;error&quot;,&quot;Please try again after some time&quot; );
}
}

Error stack trace that iam getting on second request

org.springframework.dao.DeadlockLoserDataAccessException: PreparedStatementCallback; SQL [UPDATE BALANCE set Points=points-? where EMPLOYEEID=? ]; Transaction (Process ID 94) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.; nested exception is com.microsoft.sqlserver.jdbc.SQLServerException: Transaction (Process ID 94) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:263)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:649)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:870)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:931)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:941)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:52)

I want second request to be successfull simultaneoulsy with first request without dirty read.

答案1

得分: 2

  • Isolation.SERIALIZABLE 对性能不利。异常名称具有误导性,因为它不是死锁,假设您没有其他 Isolation.SERIALIZABLE 方法的情况下,您是按照相同的顺序更新表格。

  • 死锁是指两个不同的并行操作尝试获取锁。假设 Txn1 首先涉及更新 表格 a,然后涉及更新 表格 bTxn2 首先涉及更新 表格 b,然后涉及更新 表格 a。现在如果它们都并行运行,Txn1 将锁定 表格 a 并希望锁定 表格 b,但 Txn2 已经锁定了 表格 b,并希望首先锁定 表格 a,因此它们互相等待对方放弃。但在您的示例中并非如此。所以,如果您仍然希望使用 Isolation.SERIALIZABLE,您需要捕获 DeadlockLoserDataAccessException 并在那时使用重新计算的点进行重试。

  • 您的实际代码与初始问题不同。在初始问题中,您正在增加点数,但在实际代码中,您正在用新计算的点数替换点数。因此,我认为如果您在初始问题中不使用 Isolation.SERIALIZABLE,它应该可以工作。

  • 因此,您需要传递给更新点数方法的参数是 pointsToReduct 而不是 newTotalPoints

Object[] arr = new Object[] { pointsToReduct, employeeId };
getJdbcTemplate().update("UPDATE BALANCE SET POINTS = POINTS - ? WHERE EMPLOYEEID = ?", arr);
英文:
  • Isolation.SERIALIZABLE is bad for performance. The exception name is misleading as it is not a deadlock since you are updating the tables in same sequence assuming you don't have some other Isolation.SERIALIZABLE methods.

  • Deadlock is when two different parallel operations try to acquire lock. Say Txn1 involves updating table a first and table b second. Txn2 involves updating table b first and table a second. Now if both of them run parallel, Txn1 will lock table a and wants lock on table b , but Txn2 has locked, table b first and wants lock on table a first and wait for each other to give up. But that is not the case in your example. So if you still want to proceed with Isolation.SERIALIZABLE, you need to catch the DeadlockLoserDataAccessException and retry with recalculated points at that time.

  • Your real code differs from initial question. In initial question, you are incrementing the points but in real code you are replacing the points with newly calculated points. So I think if you use the approach in the initial question without Isolation.SERIALIZABLE, it should work.

  • So the parameters you need to pass to the update points method are pointsToReduct not newTotalPoints.

    Object[] arr = new Object[] { pointsToReduct, employeeId };
getJdbcTemplate().update(&quot;UPDATE BALANCE SET POINTS = POINTS - ? WHERE 
EMPLOYEEID = ?&quot;,arr);

答案2

得分: 0

如果您正在使用Spring,并且使用REST风格的请求,您不需要担心这个问题,因为Dispatcher Servlet会逐一处理进入您的代码的并发请求。

但一个解决方案是只需添加@Transactional,然后在方法中首先执行查询以查看当前金额,然后更新为+=点数的数量。因为它在事务内部,所以数据库上会有行锁,因此您不需要担心其他人同时访问它(假设您的数据库提供程序默认提供这样的锁)。

英文:

if you are using spring, with REST style requests you don't need to worry about this as Dispatcher Servlet is going to take care of simultaneous requests coming into your code one at a time.

But one solution would be to just put @Transactional and in the method first do select to see the current amount and then update += the amount of points. Because it is inside of a transaction there will be a row lock on database so you don't need to worry somebody else accessing it at the same time. (assuming your database provider provides such locks which most would by default)

huangapple
  • 本文由 发表于 2020年8月8日 21:27:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/63315925.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定