[心缘地方]同学录
首页 | 功能说明 | 站长通知 | 最近更新 | 编码查看转换 | 代码下载 | 常见问题及讨论 | 《深入解析ASP核心技术》 | 王小鸭自动发工资条VBA版
登录系统:用户名: 密码: 如果要讨论问题,请先注册。

[备忘]多线程,给账户充值的问题

上一篇:[备忘]Spring里注入properties文件的方法
下一篇:[备忘]nutz,测试update字段自增的精度~~

添加日期:2014/11/3 6:58:40 快速返回   返回列表 阅读2484次
先看代码吧:


import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map.Entry;
import java.util.Random;
import java.util.TreeMap;

import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.impl.NutDao;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;

import cn.gbase.jiangsu.data.transfer.bean.ShopUser;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

public class TestDao {

    NutDao daoOut = new NutDao();
    MysqlDataSource dsOut = null;
    
    TreeMap<Long,String> msgMap = new TreeMap<Long,String>();

    private void setOutDataSource() {
        dsOut = new com.mysql.jdbc.jdbc2.optional.MysqlDataSource();

        // 更新连接信息
        dsOut.setServerName("localhost");
        dsOut.setDatabaseName("shop");
        dsOut.setUser("root");
        dsOut.setPassword("123456");

        // 设置数据源
        daoOut.setDataSource(dsOut);
    }

    private void doWork() throws SQLException, InterruptedException {
        setOutDataSource();

        // 初始为1
        daoOut.update(ShopUser.class, Chain.make("accountBalance", 0), Cnd.where("id", "=", 6));

        msgMap.clear();
        
        // 100个线程加1
        for (int i = 0; i < 100; i++) {
            new Thread() {
                public void run() {
                    //updateMoney();
                    //updateMoney2();
                    updateMoney3();
                    //updateMoneyJdbc();
                }
            }.start();
        }
        
        Thread.sleep(10000);
        // ShopUser u = daoOut.fetch(ShopUser.class, 6);
        // System.out.println(u.getAccountBalance());
        
        StringBuilder sb =new StringBuilder();
        for(Entry<Long,String> entry:msgMap.entrySet()){
            sb.append(entry.getKey());
            sb.append(":").append(entry.getValue()).append("\n");
        }
        System.out.println(sb.toString());

    }

    // 更新金额,使用同步,先get,再update,结果是OK的
    private synchronized void updateMoney() {

        // 随机延迟
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 读取
        ShopUser u = daoOut.fetch(ShopUser.class, 6);
        BigDecimal money = u.getAccountBalance();

        // 随机延迟
        try {
            Thread.sleep(new Random().nextInt(10));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 更新
        daoOut.update(ShopUser.class, Chain.make("accountBalance", money.add(BigDecimal.ONE)), Cnd.where("id", "=", 6));

    }

    // 更新金额,使用事务,先get,再update,结果是NG的
    private void updateMoney2() {
        // 随机延迟
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 有事务的话,多个sql执行使用的是同一个connection
        // 无事务的话,不论select还是update,每次都是新开的一个connection
        Trans.exec(Connection.TRANSACTION_READ_COMMITTED, new Atom() {
            public void run() {
                // 读取
                ShopUser u = daoOut.fetch(ShopUser.class, 6);
                BigDecimal money = u.getAccountBalance();

                // 随机延迟
                try {
                    Thread.sleep(new Random().nextInt(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 更新
                daoOut.update(ShopUser.class, Chain.make("accountBalance", money.add(BigDecimal.ONE)), Cnd.where("id", "=", 6));
            }
        });

    }
    
    // 更新金额,使用单句update,结果是OK的
    private void updateMoney3() {
        // 随机延迟,时间长一点,
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 有事务的话,多个sql执行使用的是同一个connection
        // 无事务的话,不论select还是update,每次都是新开的一个connection
        Trans.exec(Connection.TRANSACTION_READ_COMMITTED, new Atom() {
            public void run() {

                // 随机延迟,时间短一点,这样才可能在其他进程检索之前执行update
                try {
                    Thread.sleep(new Random().nextInt(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                echo("---before update ",System.nanoTime());
                
                // 更新
                daoOut.update(ShopUser.class, Chain.makeSpecial("accountBalance", "+1"), Cnd.where("id", "=", 6));
                echo("---after update ",System.nanoTime());
                
                // 随机延迟,时间短一点,这样才可能在其他进程检索之前执行update
                try {
                    Thread.sleep(new Random().nextInt(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                echo("---before update ",System.nanoTime());
                
                // 更新
                daoOut.update(ShopUser.class, Chain.makeSpecial("accountBalance", "+1"), Cnd.where("id", "=", 6));
                echo("---after update ",System.nanoTime());
            }
        });

    }

    /**
     * 试验事务级别用的,结果不太理解,可能是打印的顺序不对吧?或者是commit到打印,有时间空隙,其它事务能看到commit后数据?
     */
    private void updateMoneyJdbc() {

        // 随机延迟
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Connection conn = null; // 连接对象
        PreparedStatement pstmt = null; // 预编译的SQL语句对象
        try {

            Class.forName("com.mysql.jdbc.Driver");
            String url = "jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf-8";
            conn = DriverManager.getConnection(url, "root", "123456");

            // 事务级别
            conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
            conn.setAutoCommit(false);

            echo(Thread.currentThread().getName() + "-----after----set level-----",System.nanoTime());
            // 带参数的更新语句
            String sql = "select account_balance from shop_users where id=6";
            Statement st = conn.createStatement();
            st.execute(sql);
            st.getResultSet().next();
            BigDecimal money = st.getResultSet().getBigDecimal(1);

            echo(Thread.currentThread().getName() + "-----after----search-----" +money,System.nanoTime());
            
            // 随机延迟
            try {
                Thread.sleep(new Random().nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            echo(Thread.currentThread().getName() + "-----before----update-----",System.nanoTime());
            sql = "update shop_users set account_balance = ? where id=6";
            pstmt = conn.prepareStatement(sql);
            pstmt.setBigDecimal(1, money.add(BigDecimal.ONE));
            pstmt.execute();

            echo(Thread.currentThread().getName() + "-----after----update-----",System.nanoTime());
            // 提交事务
            conn.commit();
            
            echo(Thread.currentThread().getName() + "-----after----commit-----",System.nanoTime());
            pstmt.close();
            conn.close();
        } catch (Exception e) {
            try {
                conn.rollback(); // 回滚事务
                System.out.println("事务回滚成功,没有任何记录被更新!");
            } catch (Exception re) {
                System.out.println("回滚事务失败!");
            }
            e.printStackTrace();
        } finally {
            if (pstmt != null)
                try {
                    pstmt.close();
                } catch (Exception ignore) {
                }
            if (conn != null)
                try {
                    conn.close();
                } catch (Exception ignore) {
                }
        }
    }
    
    
    private synchronized void echo(String msg, long time){
        msgMap.put(time, msg);
    }

    public static void main(String[] args) {
        TestDao xx = new TestDao();
        try {
            try {
                xx.doWork();
            } catch (InterruptedException e) {
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

}



(1)updateMoney方法,
    先get,再update,
    不加同步的话,结果可能是17,呵呵
    加同步的话,结果是100,不用怀疑。但是,加同步不现实。
(2)updateMoney2方法,不同步,使用事务处理
    结果可能是42,呵呵
    事务使用的是Connection.TRANSACTION_READ_COMMITTED级别,
    一个事务commit后,另一个事务可以检索到。
    所以说,事务跟同步是没有关系的,别往一块想了。
------------------------------------
    事务级别说的是避免以下问题:
        脏读:一个事务未commit,另一个事务就看到了。    TRANSACTION_READ_UNCOMMITTED
        不可重复读:读了一次,再读一次,结果不一样了,比如被另一个事务改了。    TRANSACTION_READ_COMMITTED
        幻读:读了一次,再读一次,结果怎么多了,比如被另一个事务插入数据了。TRANSACTION_REPEATABLE_READ
        TRANSACTION_SERIALIZABLE
            使用TRANSACTION_READ_UNCOMMITTED时,会出现脏读。
            使用TRANSACTION_READ_COMMITTED时,会不可重复读,但可以解决脏读的问题。
            使用TRANSACTION_REPEATABLE_READ时,会幻读,但可以解决不可重复读的问题。
            使用TRANSACTION_SERIALIZABLE时,可以解决上面3个问题,但就死慢死慢了。
        每种数据库,都有个默认的事务级别,你不指定的时候,使用的就是这个级别,如:
        SQL Server :Read Commited
        Oracle:        Read Commited
        MySQL :      Repeatable Read
------------------------------------
(3)updateMoney3方法,直接使用
    UPDATE shop_users SET account_balance=account_balance+1 WHERE id=6
    结果是100,不用怀疑。
    只有一句update,已经不需要使用事务了。
    update本身会给记录加锁,保证了100次更新是排队进行的,结果OK。

(4)所以,给账户加钱这个问题,还是用单句update吧。

=========================================
可以看出READ-UNCOMMITTED隔离级别,当两个事务同时进行时,即使事务没有提交,所做的修改也会对事务内的查询做出影响,这种级别显然很不安全

READ-COMMITTED事务隔离级别,只有在事务提交后,才会对另一个事务产生影响

REPEATABLE-READ事务隔离级别,当两个事务同时进行时,其中一个事务修改数据对另一个事务不会造成影响,即使修改的事务已经提交也不会对另一个事务造成影响。

SERIALIZABLE事务隔离级别最严厉,在进行查询时就会对表或行加上共享锁,其他事务对该表将只能进行读操作,而不能进行写操作
 

评论 COMMENTS
没有评论 No Comments.

添加评论 Add new comment.
昵称 Name:
评论内容 Comment:
验证码(不区分大小写)
Validation Code:
(not case sensitive)
看不清?点这里换一张!(Change it here!)
 
评论由管理员查看后才能显示。the comment will be showed after it is checked by admin.
CopyRight © 心缘地方 2005-2999. All Rights Reserved