加锁时机
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("用户已经购买过一次"); }
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);
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("用户已经购买过一次"); }
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);
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并不能保证他们的互斥,所以这时候就用到了分布式锁