加锁时机

1,在方法上加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
//一人一单
Long userId = UserHolder.getUser().getId();

int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();

if(count>0){
return Result.fail("用户已经购买过一次");
}


//6,创建订单

VoucherOrder voucherOrder = new VoucherOrder();

long orderId = redisIdWorker.nextId("order");

voucherOrder.setId(orderId);

UserDTO user = UserHolder.getUser();
Long userid = user.getId();

voucherOrder.setUserId(userid);

voucherOrder.setVoucherId(voucherId);
save(voucherOrder);


//7,返回订单id

return Result.ok(orderId);
}

锁的范围是整个方法,锁的对象是this,任何对象来了都加这把锁,就变为了串行执行

2,以用户id为锁的key,缩小锁的粒度

1
2
3
4
5
6
7
8
@Transactional
public Result createVoucherOrder(Long voucherId) {
//一人一单
Long userId = UserHolder.getUser().getId();

synchronized (userId.toString()){}

int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();

因为userId是Long类型,所以每一个用户来时都会创建一个对象,这样同一个用户锁还是不同的,所以用值来判断,于是userId.toString(),但是看toString()方法,底层还是创建一个新的字符串,还是一个新的对象,锁还是不同,所以用到了,intern()方法。

intern():返回字符串对象的规范表示形式。
字符串池(最初为空)由类 String.
调用 intern 方法时,如果池中已包含等于该方法确定equals(Object)的此String对象的字符串,则返回池中的字符串。否则,此String对象将添加到池中,并返回对此String对象的引用。
因此,对于任意两个字符串 和 ,是当且仅当 s. equals(t) 是 时true。true s. intern() == t. intern() ts
所有文本字符串和字符串值常量表达式都将被暂存。字符串 Literals 在 Java 语言规范的 @jls 节 3.10.5 中定义。

这样值一样时返回的是同一个用户

3,由于事务问题,导致锁释放时,可能事务未提交导致新的并发问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Transactional
public Result createVoucherOrder(Long voucherId) {
//一人一单

Long userId = UserHolder.getUser().getId();

synchronized (userId.toString().intern()){
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();

if(count>0){
return Result.fail("用户已经购买过一次");
}


//6,创建订单

VoucherOrder voucherOrder = new VoucherOrder();

long orderId = redisIdWorker.nextId("order");

voucherOrder.setId(orderId);

UserDTO user = UserHolder.getUser();
Long userid = user.getId();

voucherOrder.setUserId(userid);

voucherOrder.setVoucherId(voucherId);
save(voucherOrder);


//7,返回订单id

return Result.ok(orderId);
}

}

函数执行完后,由spring,提交事务,这时,锁已经释放,但是事务,可能还未提交,这时再有同一个用户id的其他线程访问,由于数据未提交,所以访问数据库时并没有查到同一个用户id下的单,还是会产生一人多单的问题。

4,在调用该方法前加锁

1
2
3
4
5
Long userId = UserHolder.getUser().getId();

synchronized (userId.toString().intern()){
return this.createVoucherOrder(voucherId);
}

在调用该方法前加锁,就能保证,锁释放前,数据库中的内容已经改变

但是又有sping事务失效的问题:由于createVoucherOrder()方法加了事务Transactional注解,但是调用这个方法的seckillById方法没有加事务注解,在seckillById方法中用的是this调的方法,也就是说调的是VoucherOrderServiceImpl对象中的方法,它没有事务功能,而事务想要发挥作用,使用的是VoucherOrderServiceImpl的代理对象。所以我们要拿到VoucherOrderServiceImpl的代理对象。

5,使用代理对象调用create函数

5.1 获取代理对象调用方法
1
2
3
4
5
6
7
Long userId = UserHolder.getUser().getId();

synchronized (userId.toString().intern()){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();

return proxy.createVoucherOrder(voucherId);
}
5.2 暴露接口
1
2
3
4
5
6
public interface IVoucherOrderService extends IService<VoucherOrder> {

Result seckillById(Long voucherId);

Result createVoucherOrder(Long voucherId);
}
5.3 添加依赖
1
2
3
4
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
5.4 启动类上添加注解EnableAspectJAutoProxy
1
2
3
4
5
6
7
8
9
10
@MapperScan("com.hmdp.mapper")
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class HmDianPingApplication {

public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}

}

6,集群情况下,单机锁失效问题

由于我们使用的是synchronized,它在一个jvm内可以,监控各个线程,完成互斥操作,但是在集群情况下,多个jvm,synchronized并不能保证他们的互斥,所以这时候就用到了分布式锁