乐观锁
MySQL的默认存储引擎是InnoDB,默认行锁。所以在update的时候会在对同一条记录加锁。虽然存在两个线程的update的事务未提交,但是innodb的update操作是采用当前读的方式。假设线程A开启事务A并执行update操作后,但未提交事务;此时线程B也开启事务B并update操作。此时线程B的update是基于当前读的方式获取到线程A更改后的结果。如果事务A在此时想提交事务,这是不允许的。根据两阶段锁协议,事务B没提交,因为此时在这条记录上的写锁还没有释放。所以必须等待事务B提交后释放锁。
超卖
事务只有写锁,并发的时候多个线程都可以读到有足够的库存 比如大家都读取有4件库存
现在用户并发的购买一件商品时
因为事务只是写锁,线程门排队一个个进行写数据,每个线程都将库存数据更新为3 (没错,第一个人将4改成3, 第二个人将3改成3, 第三个人将3改成3,以此类推。 然后一件商品被卖了上万件) // 秒杀商品亏死你
你要明白,读和写是两个操作。 事务只有写锁,没有读锁
悲观锁
update语句后面接for update 这个是读写锁。
// 第一个线程读取到后,不允许别人在去读。 只有第一个线程完成事务后,第二个线程才能读
//比如你在下单时,因为是读锁,锁住商品不让别人去读,然后其他人在首页刷新商品时就刷新不出来了,因为库存被你读锁锁住了,他们读取不到库存。 你一个人成功的把整个网站给堵住了。 超级厉害。 性能超低,所以也不推荐使用
乐关锁
其原理就是利用事务本身就是写锁来实现的
我在这里举例一个金额扣减的例子,你理解这个案例了,那么对于库存的扣减也就理解了
开启两个事务,在这两个事务中同时对数据库表的同一行数据进行更新,则第一个事务的update语句先执行,而第二个事务的update语句后执行,那么要等到第一个事务提交之后,第二个事务的更新语句才能从阻塞状态变成执行状态。
在上面的图中,左边的事务执行完余额更新操作后commit之后,虽然右边的事务中执行的查询操作,每次都是查询都是20,(当前的事务隔离级别为可重复读)但是执行更新操作后变成了-20,此时我们应该添加乐观锁*,且不能通过java代码进行判断,直接更新某个变量为绝对值,而应该是相对值。
update account set balance=balance-20 where id =1; // 需要添加乐观锁
update account set balance=balabce-20 where id =1 and balance>0;
// 不能在java代码中更具select取出的值为20,然后通过if判断20>0就,简单的调用update字段语句
if(balance>0){
remain = balance-20
executeSql(“update account set balance = remain where id =1 and balance>0”)
因为隔离级别为可重复读,此时读取的balance都是进入事务之前的值,却不是最新的,所以我们的sql语句使用的变量应该是由数据库控制的变量如balance = balance-20,而不是通过java代码读取的balance进行计算,通过数据库的变量能够进行更新,如上面最后出现的-20;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void decreaseItemSpecStock(String specId, int buyCounts) {
// synchronized 不推荐使用,集群下无用,性能低下
// 锁数据库: 不推荐,导致数据库性能低下
// 分布式锁 zookeeper redis
// lockUtil.getLock(); -- 加锁
// 1. 查询库存
// int stock = 10;
// 2. 判断库存,是否能够减少到0以下
// if (stock - buyCounts < 0) {
// 提示用户库存不够
// 10 - 3 -3 - 5 = -1
// }
// lockUtil.unLock(); -- 解锁
int result = itemsMapperCustom.decreaseItemSpecStock(specId, buyCounts);
if (result != 1) {
throw new RuntimeException("订单创建失败,原因:库存不足!");
}
}
<!--乐观锁-->
<update id="decreaseItemSpecStock">
update
items_spec
set
stock = stock - #{pendingCounts}
where
id = #{specId}
and
stock >= #{pendingCounts}
</update>
redis锁
@Component
@Slf4j
public class RedisLock {
/**
* 解锁脚本,原子操作
*/
private static final String UNLOCKSCRIPT =
"if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
+ "then\n"
+ " return redis.call(\"del\",KEYS[1])\n"
+ "else\n"
+ " return 0\n"
+ "end"; //NOSONAR
private StringRedisTemplate redisTemplate;
public RedisLock(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 加锁,有阻塞
* @param key key
* @param expire 过期时间
* @return
*/
public String lockNoSave(String key, long expire){
String token = tryLock(key, expire);
return token;
}
/**
* 加锁,有阻塞
* @param key key
* @param expire 过期时间
* @param timeout 释放时间
* @return
*/
public String lock(String key, long expire, long timeout){
long startTime = System.currentTimeMillis();
String token;
do{
token = tryLock(key, expire);
if(token == null) {
if((System.currentTimeMillis()-startTime) > (timeout - 50)) {
break;
}
try {
//try 50 per sec
Thread.sleep(50);
} catch (InterruptedException e) {
log.error("redis lock error", e);
Thread.currentThread().interrupt();
return null;
}
}
}while(token==null);
return token;
}
/**
* 加锁,无阻塞
* @param key key
* @param expire 过期时间
* @return
*/
public String tryLock(String key, long expire) {
String token = UUID.randomUUID().toString();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
assert factory != null;
RedisConnection conn = factory.getConnection();
try{
Boolean result = conn.set(key.getBytes(StandardCharsets.UTF_8), token.getBytes(StandardCharsets.UTF_8),
Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
if(result!=null && result) {
return token;
}
}finally {
RedisConnectionUtils.releaseConnection(conn, factory,false);
}
return null;
}
/**
* 解锁
* @param key key
* @param token token
* @return
*/
public boolean unlock(String key, String token) {
byte[][] keysAndArgs = new byte[2][];
keysAndArgs[0] = key.getBytes(StandardCharsets.UTF_8);
keysAndArgs[1] = token.getBytes(StandardCharsets.UTF_8);
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
assert factory != null;
RedisConnection conn = factory.getConnection();
try {
Long result = (Long)conn.scriptingCommands().eval(UNLOCKSCRIPT.getBytes(StandardCharsets.UTF_8), ReturnType.INTEGER, 1, keysAndArgs);
if(result!=null && result>0) {
return true;
}
}finally {
RedisConnectionUtils.releaseConnection(conn,factory,false);
}
return false;
}
}
使用Redis锁
try{
token = redisLock.lockNoSave(id.toString(),15000);
if(token == null){
throw new ApplicationException("添加中,无需重复添加!");
}
//可以卖东西
//可以更新数据
//可以保存需要保存的数据
}finally {
if(token != null) {
redisLock.unlock(aiAlgorithmResultsId.toString(), token);
}
}