mybatis与jdbc最主要的区别就是mybatis用的是sqlsession,其实作用都是一样的,就是数据库的一个连接,那么sqlsession是不是线程安全呢?答案是否定的,
以下是测试过程:
这里有两个语句:查询和修改:
当查询和修改放在controller下,也就是没放在一个事务下的时候,会创建两个sqlsession
而这两个语句放在service 层中并开启注解后会发现:
后来的一个sql会获取前边的sqlsession并使用:
也就是说,获取的连接是一个,但是这样能否保证线程安全呢?
于是我做了以下测试:
先进行查询然后,在1秒内往数据库插入100条数据,通过jmeter实现:
于是数据库出现了这样的场景:
可以看到数据全是一样的,这说明连接的一致与线程安全无关,那怎么解决这样的线程安全呢?
什么是锁?我的理解是锁就是唯一的一个东西。
首先想到的是锁:
1.synchonized和lock都可以实现,但是加锁会消耗性能。
2.再就是 threadlocal,这个可以实现线程隔离但是在这里需要的是线程竞争。
3.redis锁(适合分布式),下面这个可以简单实现,利用springboot自动装配的StringRedisTemplate,因为redis就是单线程的,所以通过以下这个setIfAbsent原子方法可以实现redis锁
别忘了在最后删除锁,但是下面这个代码有几个问题。
1.第一行加锁成功后宕机了,结果所有线程都阻塞了,可以通过设置超时时间释放锁,但是这样也有问题:如果一个线程在时间内没有执行完业务就把锁释放了,会导致锁失效,
这种可以创建一个线程用来异步完成续命,就是通过一个while循环,设置锁的时常,注意一定要另开一个线程,不然就会一直阻塞在续命这里。
2.设置时间后假设时间是2s,2s后线程1没执行完,但是锁释放了,线程2获得了锁,执行代码,这个时候线程1执行完了代码,然后删除了这个锁,但是这个锁是线程2的,
这个可以在删除锁之前加一个判断,通过UUID进行实现,判断前后执行的线程是否一致,一致的话就可以删除锁,然后这个uuid怎么放呢?首先在加锁 的时候把uuid放在redis和ThreadLocal中,
这样在释放锁的时候从ThreadLocal中拿到uuid,跟redis中的值做判断,如果是一个的话就说明是这个线程。
3.非阻塞,如果同时有100个线程访问,那么一个线程获取锁之后其他的就return了,不会继续等待,这种适合于有1000个数量,然后由100个线程这样的差距比较大的场景,如果改成阻塞的话把获取锁的代码放到while循环就可以。
4.不可重入,如下代码假设在加锁后还有另一个业务,也是需要加锁的,这样的话就会加不上锁,导致不可重入,这个解决办法是在加锁前做一个判断,先从ThreadLocal中取数据,如果取不到的话就加锁,取到的话呢就把boolean值的锁设置为true,这样就可以继续加锁了。
5.以上所有的问题都可以用redisson来解决。
4.还有CAS无锁,首先理解一下jvm的概念,在controller中每一次请求就是一个线程,controller中的变量什么的都属于线程的内部变量,彼此不可见这个无所谓但是
下面这个方法是在service层中,线程没通过这里时update方法就在方法区中,而线程通过时就会在各自的虚拟机栈上创建一个栈帧,由于JMM策略,在每个栈帧的局部变量表中
都获取到一个同样的变量n,忽略操作数栈的操作,这里的n就是0,然后下面这个就是cas锁了,底层是unsafe包下用c++写的一个原子操作,意思是比较并交换,想象一下:
有两个线程获取同样的 n,j,当第一个线程进入cas方法后发现n和内存中的i是相等的,然后就把 j 赋值给 i,然后才能进行下一步操作,这个时候下一个线程也比较,但是这时内存中 i 已经变为 1
了,于是进入循环中,重新获取 内存中 i 的值,再 比较交换,整个流程就是这样的,我说的流程有先后,但是实际上是同时比较交换的,因为 i 是一个原子类所以不会出错。
模型:
7.还有通过volatitle变量,通过排它锁拿到这个变量,这个方法试了貌似可行,后续再研究一下。
8.最近在项目中用公司的jpa框架的@version注解实现了乐观锁,这种方法比较简单直接,可以在mybatis中借鉴一下。
原理:
1.先执行一个查询操作,拿到数据库中的乐观锁字段,假设这个乐观锁的字段是1,假设如果有两个线程都拿到了这个1的乐观锁。
2.线程1执行修改操作,在修改的时候对比id和这个乐观锁字段,如果跟之前拿到的一致则修改成功,把version这个乐观锁字段加一
update 表 set **=**,version = version+1 where id = * and version =*(上一步拿到的乐观锁的值)
3.线程2修改的时候where 条件发现version跟之前拿到的锁对不上了,所以修改失败。
在jpa中这个查询操作通过@version注解隐藏了,但是确实是存在的。