場景如下:
用戶賬戶有余額,當發生交易時,需要實時更新余額。這里如果發生並發問題,那么會造成用戶余額和實際交易的不一致,這對公司和客戶來說都是很危險的。
那么如何避免,
有以下兩種方法:
1、使用悲觀鎖
當需要變更余額時,通過代碼在事務中對當前需要更新的記錄設置for update行鎖,然后開始正常的查詢和更新操作
這樣,其他的事務只能等待該事務完成后方可操作
當然要特別注意,如果使用了Spring的事務注解,需要配置一下:
<!-- (事務管理)transaction manager, use JtaTransactionManager for global tx --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 使用annotation定義事務 --> <tx:annotation-driven transaction-manager="transactionManager" />
在指定代碼處添加事務注解
@Transactional @Override public boolean increaseBalanceByLock(Long userId, BigDecimal amount) throws ValidateException { long time = System.currentTimeMillis(); //獲取對記錄的鎖定 UserBalance balance = userBalanceDao.getLock(userId); LOGGER.info("[lock] start. time: {}", time); if (null == balance) { throw new ValidateException( ValidateErrorCode.ERRORCODE_BALANCE_NOTEXIST, "user balance is not exist"); } boolean result = userBalanceDao.increaseBalanceByLock(balance, amount); long timeEnd = System.currentTimeMillis(); LOGGER.info("[lock] end. time: {}", timeEnd); return result; }
通過db的悲觀鎖,實際測試該方法確實可以有效控制,不過在大並發量的情況下,可能會有性能問題
<select id="getLock" resultMap="BaseResultMap" parameterType="java.lang.Long"> <![CDATA[ select * from user_balance where id=#{id,jdbcType=BIGINT} for update; ]]> </select>
2、使用樂觀鎖
這個方法也同樣可以解決場景中描述的問題(我認為比較適合並不頻繁的操作):
設計表的時候增加一個version(版本控制字段),每次需要更新余額的時候,先獲取對象,update的時候根據version和id為條件去更新,如果更新回來的數量為0,說明version已經變更
需要重復一次更新操作,如下:sql腳本
update user_balance set Balance = #{balance,jdbcType=DECIMAL},Version = Version+1 where Id = #{id,jdbcType=BIGINT} and Version = #{version,jdbcType=BIGINT}
這是一種不使用數據庫鎖的方法,解決方式也很巧妙。當然,在大量並發的情況下,一次扣款需要重復多次的操作才能成功,還是有不足之處的。
延伸:
樂觀鎖:對每次的數據操作都保持樂觀的態度,因此不對數據進行上鎖。那么就存在數據會被反復讀寫的情況,所以每次修改數據的時候需要對數據進行判斷是否被修改過。
悲觀鎖:對每次的數據操作持悲觀態度,操作時上鎖,防止操作時數據被他人修改
使用場景:
樂觀鎖:由於不上鎖,性能較好,適用於讀大於寫的情況,如果寫較多,則會導致重復嘗試寫入均失敗。
悲觀鎖:上鎖,數據寫入時會導致讀被掛起,適合寫大於讀的場景
實現:
樂觀鎖:在db中,可以增加版本號控制,java中的CAS等
悲觀鎖:db的for update,java中的synchronize等