Redis代替session需要注意:
1.选择合适的数据结构
2.选择合适的key
3.选择合适的存储粒度
Redis缓存策略一般采用先更新数据库后删除缓存,原因是无论先删除缓存还是后删除缓存都有线程安全的风险,但是后删除缓存的发生风险的概率小到可以忽略不计,
因为缓存的更新是微秒级的,理论上首先在缓存要突然失效时,A线程查询数据库获取到旧数据,并缓存更新的时间段内,
B线程将新的数据在数据库里面更新并执行完缓存的删除操作之后,A线程再将旧数据写入缓存内,才会发生线程安全问题,但A线程更新缓存的时间极短,
这段时间不足以让B线程执行完操作,所以这个概率极小。
同时还需要保证数据库与缓存操作的原子性,单体系统通过事务机制解决,分布式系统利用分布式事务机制解决。
缓存穿透
1.缓存一个空对象,每次返回一个空,缺点就是有效期内有额外的内存开销。可能会短期内的数据不一致
2.布隆过滤
在redis前加一个布隆过滤器,原理是数据库里面的数据基于某种哈希算法得到哈希值,再将哈希值转化为二进制位,保存到布隆过滤器里。有点是数据量小,
不用保存多余的key,缺点是实现复杂,有误判的可能。
实际使用一般用第一种方案
其他还有考虑增加id复杂度,避免被猜到规矩,做好数据的基础格式校验,加强用户权限,做好热点参数限流比如空值这种
缓存雪崩
同时大量key失效或者redis服务宕机导致数据库压力巨大
1.给不同的key添加TTL随机值,达到不同的失效时间
2.提高redis集群高服务的可用性,比如哨兵机制,和redis数据同步(主从服务器)
3.给缓存业务添加降级限流策略,比如提前做好准备,服务器挂了可以快速返回失败等
4.给业务添加多级缓存,比如nginx也做缓存
缓存击穿
一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求在一瞬间给数据库带来了巨大的冲击。比如我这个业务重建时间为两秒,在两秒之内有上百次访问,
重建流程都会走上上百次。
1.互斥锁,在线程里加锁,第一个获取到锁之后其他的线程休眠等待,重试,构建完成之后再进行。优点是没有额外的内存消耗,保证一致性,实现简单。缺点有线程等待,
性能收影响。还有死锁的风险
2.逻辑过期
不添加TTL,在value值里面绑定过期时间。取出数据后判断数据是否过期。过期后同样需要加锁重建,第一个获取到锁的对象会开启一个新线程处理重建逻辑,重建完成后再释放锁。
其他的线程都返回旧数据。优点是无线程等待,性能好。缺点有不保证一致性,有额外的内存消耗,实现相对复杂
根据实际需求选择合适的方案
超卖
1.加悲观锁,比如synchronized关键字,lock锁,数据库互斥锁都属于悲观锁。好处锁的稳当,坏处性能低下
2.加乐观锁,乐观锁在查询的时候不加锁,任何人都能查,只在修改数据库的时候验证是否已修改,如修改就重试或抛异常。
操作方式
1.版本号法也叫cas法。原理:在数据库增加一个版本号字段,每次查询的时候把版本号也查出来,修改的时候同时修改库存和版本号,在where条件中把版本号带上,
如果在同一次流程中之前的版本号已经被修改,那么该次更新失败。
该法可简化为用库存本身代替版本号,这种方技术上没问题,但业务上则显得或许保守,购买失败率高,可将where条件中的stock=stock改为stock>0即可。
如果特殊场景下只能使用stock=stock这种写法,那么可以采用数据库分表,把数据分别存入多张表中,并发时操作不同的表即可
一人一单
数据修改可以使用乐观锁处理,但数据新增无法使用乐观锁处理,只能加锁。原因是线程1在查询订单后判断订单前,线程2也来查询订单,这个时候线程1和线程2都无法查询到新订单,
所以都认为可以插入新订单,这样同一个用户就多次购买同一件商品。在单服务器的情况下可以使用synchronized关键字等方式加锁。但在集群环境下有多个jvm虚拟机,
synchronized关键字就不适合。必须使用分布式锁来处理。
分布式锁
满足分布式系统或集群模式下多进程可见并互斥的锁。一般具备多进程可见,互斥,高可用,高性能,安全性等几个纬度。
常见有mysql、redis、zookeeper等
Redis分布式锁实现原理,添加锁,setnx lock thread1 然后再添加过期时间 expire lock 10。释放锁的时候直接删除该锁即可。del key
基本原理中存在几个问题需要优化处理
1.如果线程1在执行时因为某种原因卡顿,导致了锁过期了,线程2就会进来重新加锁执行线程2的业务,当线程1恢复后释放锁的时候,就会把线程2的锁给释放掉,
这样就会引发线程安全问题。解决方案,在释放锁的时候判断这个锁是不是这个线程的,如果是才释放,不是就表示这个锁过期了直接跳过不管。
(存入的锁的value值就是这个线程的公共id,这个id是集群唯一的,一般用uuid来表示)
2.当使用上面的方案优化分布式锁之后会引出第二个问题,就是线程1在释放锁的时候,先判断锁是否是自己的,然后gc执行垃圾回收,然后再释放锁。
当gc在执行垃圾回收的时候,会阻塞所有的java程序执行。如果阻塞时间超过了剩余超时时间,那么锁会超时释放,这样线程2会加锁并执行业务逻辑。
当线程1阻塞结束后,继续执行任务时,由于先判断了锁是否为自己的,所以就会直接执行删锁命令。这样就会把线程2的锁删掉,从而引发线程安全问题。
解决方案:采用lua脚本来执行加锁解锁操作,这样由于执行者不是java虚拟机而是cpu就绕过了gc阻塞实现了任务的原子性。Redisson框架也是采用lua脚本来解决这个问题的,
lua脚本是redis本身支持的一个脚本功能
Redisson
是一个在redis基础上实现的java驻内存数据网格,它不仅提供了一系列的分布式的java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现
Redisson可重入锁原理:
业务场景:同一个线程,调用方法1,获取了锁,在方法1中,调用方法2,方法2也获取锁,如果这把锁不可重入,那么在调用方法2时会获取锁失败。
可以使用hash结构来实现可重入锁,key锁名称,field所属线程,value重入次数。
如果同一个线程重复获取锁只需要把重入次数加一即可,释放锁的时候,需要判断重入次数,如果多次获取了锁,需要把锁重入次数减一,直到次数为1时,再次释放即可删除锁。
这个功能由于判断很多,需要使用lua脚本来保证每个方法的原子性,否则容易引起线程不安全
Redisson分布式锁原理:
1.可重入:利用hash结构记录线程id和重入次数
2.可重试:利用信号量和pubsub功能实现等待、唤醒、获取锁失败的重试机制
3.超时续约:利用watchdog,每隔一段时间(releaseTime),重置超时时间
总结:
1.不可重入Redis分布式锁
原理:利用setnx的互斥性,利用ex(过期时间)避免死锁,释放锁时判断线程标识。
缺陷:不可重入,无法重试,锁超时失效
2.可重入的redis分布式锁
原理:利用hash结构,记录线程标识和重入次数,利用watchDog延续锁的过期时间,利用信号量控制锁重试等待
缺陷:redis宕机引起锁失效问题
3.Redisson的multiLock
原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
缺陷:运维成本高,实现复杂
本文详细探讨了Redis作为缓存的使用策略,包括数据结构选择、存储粒度和缓存更新策略。提到了先更新数据库后删除缓存以降低线程安全风险,并讨论了缓存穿透的解决方案,如布隆过滤器。此外,文章还分析了缓存雪崩和缓存击穿的应对措施,以及分布式锁的实现和优化,包括Redisson的可重入锁和分布式锁原理。
1150

被折叠的 条评论
为什么被折叠?



